<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="mobile-web-app-capable" content="yes">
    
    
    <meta name="description" content="# 实验教程 2.0 ![](https://pad.degrowth.net/uploads/upload_0480d5c2991859cce4c1840f3c80aaf3.png) &gt; 待补充，数">
    
    
    
    <meta property="og:title" content="实验教程 2.0 - CodiMD">
    
    
    
    <meta property="og:type" content="website">
    
    <meta property="og:image" content="https://pad.degrowth.net/codimd-icon-1024.png">
    <meta property="og:image:alt" content="CodiMD logo">
    <meta property="og:image:type" content="image/png">
    
    <base href="https://pad.degrowth.net/">
    <title>实验教程 2.0 - CodiMD</title>
    <link rel="icon" type="image/png" href="https://pad.degrowth.net/favicon.png">
    <link rel="apple-touch-icon" href="https://pad.degrowth.net/apple-touch-icon.png">
  
    <link rel="stylesheet" href='https://pad.degrowth.net/build/emojify.js/dist/css/basic/emojify.min.css'>
    <link href="build/font-pack.css" rel="stylesheet"><link href="build/pretty-styles-pack.css" rel="stylesheet"><link href="build/pretty-styles.css" rel="stylesheet"><link href="build/pretty.css" rel="stylesheet">
  
</head>

<body style="display:none;">
    <div class="ui-infobar container-fluid unselectable hidden-print">
        <small>
            <span>
                
                  <span class="ui-no-lastchangeuser">&thinsp;<i class="fa fa-clock-o fa-fw" style="width: 18px;"></i></span>
                
                &nbsp;<span class="text-uppercase ui-status-lastchange"></span>
                <span class="ui-lastchange text-uppercase" data-createtime="Fri Feb 25 2022 07:21:32 GMT+0000 (Coordinated Universal Time)" data-updatetime="Wed Nov 16 2022 10:45:22 GMT+0000 (Coordinated Universal Time)"></span>
            </span>
            <span class="pull-right">44 views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-fw fa-pencil"></i></a></span>
            <br>
            
            <span class="ui-owner">
                &thinsp;<i class="ui-user-icon small" style="background-image: url(https://cdn.libravatar.org/avatar/95bbdcb580ff484dcff50b1458c95b31?s=96);" data-toggle="tooltip" data-placement="right" title="zxli"></i>
                &nbsp;<span class="text-uppercase">owned this note</span>
            </span>
            
        </small>
    </div>
    <div id="doc" class="container markdown-body"># 实验教程 2.0
![](https://pad.degrowth.net/uploads/upload_0480d5c2991859cce4c1840f3c80aaf3.png)
&gt; 待补充，数据库课程，同时涉及关系型数据库和时序数据库
&gt; 待补充，组成原理能不能有内容可以引进来
&gt; 待补充，后面更换为RISC-V的板子，我们两套方案共存，请胡京和司子猷将其参加比赛的内容融合进来
&gt; 
## 系统架构
在该实验教程中，我们将从零开始，搭建一个无线物联网系统，在这个系统的帮助下，我们可以实时的监控学院大楼的环境情况，随时发布自己想要的应用，例如人脸识别、口罩识别、数据聚合等，并根据算力和应用的需求合理的分配应用的部署位置。

在其中将学习到计算机组成原理、计算机网络、物联网、人工智能、网络安全等诸多知识。该系统在传统的物联网架构下，引入了云原生边缘计算，网络中的每一个结点将拥有一定的计算能力，用来部署一些应用，其系统架构如下：

![](https://pad.degrowth.net/uploads/upload_03080ae02ac8419caacd997efa0e4917.png)

为了完成这一系统，我们需要以下硬件：
树莓派4b：3个以上
STM32开发板：与树莓派一一对应
杜邦线：若干
OpenWRT开发板：1个
传感器：烟雾、光照、噪声、雷达等
Lora天线：与树莓派一一对应

如上图所示，整个系统由两种节点组成，树莓派节点由一个树莓派作为主控板，用于采集、存储数据、部署应用，OpenWRT节点用于网络监控、路由等。其中最主要的为树莓派节点，节点具体架构如下：

![](https://pad.degrowth.net/uploads/upload_ca66bcf1c31ffa53e9a556f14f5b5467.png)

如图中所示，每个节点包含四个主要部分：STM32、树莓派、lora天线、电源。STM32负责连接传感器采集数据，通过串口将数据转发至树莓派上，树莓派负责接受处理数据，部署应用，lora天线负责远距离通信。每位同学都将在本科学习期间独立完成一个节点的内容，并和其他同学组成网络。
完成后的实物图如下：
![](https://pad.degrowth.net/uploads/upload_f221bc3f8bc057d095e3b0f64d5dc7cf.jpg)


## 第一章 传感器部署（这里先不加入树莓派）
  那么废话不多说，我们开始正式的搭建这个系统。既然是物联网系统，那么便少不了传感器。在这一章中我们将介绍如何部署各种各样的传感器，部署完成后如何获取传感器采集的数据。为了完成这一目标，我们将使用STM32开发板来获取传感器采集到的数据，STM32在获取到传感器采集到的数据后，将会把数据发送给树莓派，由树莓派来处理这些数据。因此传感器、STM32以及树莓派的连接图如下：
（插入图片）

  了解到这些以后，我们就可以动手开始我们的实验了。

  传感器是一种检测装置，能感受到各种各样的信息，并能将感受到的信息，按一定规律变换成为电信号或其他所需形式的信息输出，以满足信息的传输、处理、存储、显示、记录和控制等要求。传感器可以用来检测各种各样的环境信息，因此在物联网系统中，传感器得到了非常广泛的应用。在我们的无线物联网系统中，传感器收集到的周围环境数据是整个系统的数据来源，下面我们首先介绍如何使用各种不同的传感器来采集周围的环境数据的实验。

### 实验一 传感器部署

#### 实验目标

  本次实验的目标是使用传感器采集周围环境数据，并且可以获取到传感器采集的数据。首先我们先明确一点，传感器一般不是单独使用的，而是和一些开发板（例如STM32、树莓派、Arduino等等）结合使用的。本次实验中我们将传感器部署到STM32开发板上，使用STM32开发板来收集传感器采集的数据。在完成本次实验后，你将可以使用STM32读取并收集传感器采集到的周围环境数据。

  在开始实验之前，我们先对本次实验中涉及到的一些概念进行简单介绍。

#### 实验原理

**STM32简介**

STM32单片机主要是由意法半导体公司设计的微控制器,其具有低功耗、低成本和高性能的特点,适用于嵌入式应用。其采用ARM Cortex-O内核,根据其内核架构的不同,可以将其分成一系列产品,当前主流的产品包括STM32F0 STM32F1、STM32F3,具有超低功耗的产品包括STM32L0、STM32L1、STM32L4等。STM32属于一个微控制器，自带了各种常用通信接口，比如USART、I2C、SPI等，可接非常多的传感器。因此在本实验中我们将传感器连接到STM32上来读取传感器数据。

**ADC简介**

ADC是Analog-to-Digital Converter的缩写。模拟/数字转换器。是指将连续变量的模拟信号转换为离散的数字信号的器件。其基本步骤如下：（1）首先在一个选定的瞬间对模拟信号进行采样。（2）然后再将取样值转换成数字量。（3）按照一定的编码格式给出转换结果。
本次实验中使用的STM32F103系列开发板中的12位ADC是一种逐次逼近型模拟数字转换器。有三个ADC,每个ADC有多达18个通道（通道可以理解为数据通路，能够完成内存与外设之间数据的传输），可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右对齐方式存储在16位数据寄存器中。

**交叉编译**

在介绍交叉编译之前，我们先了解一下编译。由编译器把高级语言代码编译为汇编代码的过程称为编译。源代码经过编译后，并没有生成最终的可执行文件，而是生成目标文件(Object File)，然后由链接器将这些目标文件链接起来生成可执行文件。与交叉编译相对应的是&#34;本地编译（native compile&#34;）。本地编译是指编译源代码的平台和执行源代码编译后程序的平台是同一个平台。相应的，交叉编译指编译源代码的平台和执行源代码编译后程序的平台是两个不同的平台。那么为什么要有交叉编译呢？直接在同一个平台上编译和执行程序不就行了吗？主要原因是：对于一些嵌入式设备，出于低成本和低功耗的考虑，往往性能会很低，没有足够的资源来进行编译或者编译的过程非常慢。因此直接将编译好的程序放到这些设备上执行会比较合适。


#### 实验步骤

本次实验是将传感器连接到STM32开发板上，需要编写、编译相应的程序用来读取传感器采集的数据。但是由于STM32开发板系统资源有限，并且输入显示也受限，因此不适合做为开发载体。因此需要使用到交叉开发的方式，因此第一步我们先在上位机上（电脑上）搭建交叉编译环境。在此之前我们简要介绍一下交叉编译，交叉编译可以理解为，在当前编译平台下，编译出来的程序能运行在体系结构不同的另一种目标平台上，但是编译平台本身却不能运行该程序。本次实验中我们的编译平台系统为win10，编译好后的程序可以在stm32开发板上运行。下面我们来介绍如何搭建交叉编译环境。

1、交叉编译环境搭建(linux与windows环境下使用IDE不同，其余操作步骤基本相同，在此以windows环境下为例介绍其操作过程)
1.1 IDE下载与安装（windows环境）
目前STM32开发常用的IDE有IAR、MDK等，本次实验中使用的MDK，下载地址为：https://www.keil.com/download/product/ ，下载界面如下：
![](https://pad.degrowth.net/uploads/upload_7d0c695712b094faa3f084f4a0f08854.png)
下载完成后，按照提示进行安装，安装完成后我们就可以进行下一步操作。
IDE下载与安装（linux环境）
在Linux环境下，MDK没有linux版本，因此我们使用的IDE为TrueStudio，下载地址为：https://atollic.com/resources/download/linux/ 下载界面如下：
![](https://pad.degrowth.net/uploads/upload_a9caf9b55c6f0772fa8d6b67477b79a6.png)


1.2 安装需要芯片包
本次实验中使用的STM32开发板为STM32F103系列开发板，首先根据我们的开发板型号去官网下载ST官方库，使用到的ST官方库为：STM32F10x_StdPeriph_Lib_V3.5.0，下载地址为：https://www.st.com/en/embedded-software/stsw-stm32054.html#design-scroll ，解压后文件中的内容如下：
![](https://pad.degrowth.net/uploads/upload_022a7ffe8ff9f3b9bd34409c5f819065.png)
新建一个Project文件夹，并在里面新建三个文件夹，CMSIS（存放内核函数及启动引导文件），FWLIB（存放库函数），USER（存放用户自己的函数），然后将STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport中的文件复制到CMSIS中，再将C:STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x中的文件全部复制到CMSIS中。将STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver中的 inc src 复制进FWLIB文件夹。将STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template中的这几个文件复制进USER。至此，我们需要的文件包已经准备好了。
下面我们打开第一步中安装好的KEIL，新建一个工程,保存在刚才新建的project文件夹下。保存后会让选择芯片，本次实验使用的是STM32F103ZE系列，选择相应的芯片。
![](https://pad.degrowth.net/uploads/upload_55561570644c8d9dd79d325ca0953c55.png)
选择好后会弹出一个新的窗口，直接点击cancel退出。然后点击下图中红色框选中的图标管理项目文件结构。
![](https://pad.degrowth.net/uploads/upload_c4c0d426dfc3c1a4b3323e44486570e0.png)

依次添加CMSIS、USER、FWLIB、STARTUP，并把刚刚复制在对应文件夹下的文件添加。注意STARTUP要选择Project\CMSIS\startup\arm中的startup_stm32f10x_hd.s、startup_stm32f10x_ld.s、startup_stm32f10x_md.s三个。FWLIB中只需要添加src中的.c文件即可。CMSIS中不需要添加STARTUP中的文件。添加完毕点击OK。修改后的文件内容如下：
![](https://pad.degrowth.net/uploads/upload_5e42b5bd87df079a94f91a75ddd2dcad.png)
然后点击下图中红框框中的图标，进入C/C++设置界面，在define一栏输入 STM32F10X_HD,USE_STDPERIPH_DRIVER ，并在includepath栏加入头文件路径。再进入Target设置界面，修改ARM Compiler一栏为：Use default compiler version5,并选中Use MicroLIB。最后进入Output设置界面，选中Creat HEX File。
![](https://pad.degrowth.net/uploads/upload_df57b36931b31a5d859dce8dc989c97c.png)
修改后的部分内容如下：
![](https://pad.degrowth.net/uploads/upload_d751710d7cc3de021421b02f7282e1cd.png)
至此，工程创建完毕。接下来可以在USER文件夹下添加、编辑你自己的代码。

2、上一步中我们已经搭建后STM32开发环境了，这一步中我们首先介绍如何将传感器与STM32连接，然后编写相应的程序来读取传感器采集到的数据。在我们的物联网系统中使用到了光敏电阻传感器、可燃气体传感器、火焰检测传感器以及温度传感器，不同的传感器读取流程可能会不一样，可以参考STM32相关文档以及相应的传感器说明进行操作，本次实验中我们已光敏电阻传感器来详细说明具体操作流程。
2.1 光敏电阻传感器与STM32开发板连接
光敏电阻传感器实物图如下：
![](https://pad.degrowth.net/uploads/upload_6a0d88595693c4d50f1de03231241409.jpg)
该传感器模块一共有四个针脚，其中VCC和GND分别接电源正极（3.3~5V）和接地。对于DO端，当模块在环境光线亮度达不到设定阈值时，DO端输出高电平，当外界环境光线亮度超过设定阈值时，DO端输出低电平；板拟信号输出端AO可以和AD模块相连，通过AD转换，可以获得环境光强更精准的数值。在本次实验中，我们使用AO端与STM32的ADC模块相连，通过STM32的ADC来获取周围环境光照强度的具体数值。可以查看STM32芯片ADC通道和引脚对应关系图（如下图）确定传感器如何与STM32相连。
![](https://pad.degrowth.net/uploads/upload_f89557b145d31cc4ed64a7901ebd69f1.png)
在这次实验中，我们通过ADC3中的通道3来获取传感器采集到的数据(ADC1中的通道16和17已经被温度传感器和内部参照电压占用，我们可以选择除此之外任意ADC中的任意一个通道来获取数据)。因此需要将光敏电阻传感器的AO端与STM32的PA3引脚相连，VCC和GND分别接STM32的5V或3.3V引脚，GND与STM32的GND引脚相连。连接实物图如下：
![](https://pad.degrowth.net/uploads/upload_2c7547a8e672b1aa1dd140478a93c03c.jpg)
连接完成后，我们就可以开始编写代码来获取传感器的数据了。
2.2 关键部分代码
流程:1、初始化相关引脚，初始化ADC模块。2、获取ADC对应通道的采样值。3、多取几次ADC通道采样值求平均值。4、根据相关转换公式，将获取到的ADC采样平均值转换为具体的光照强度数值。
ADC模块获取采样值流程：1、开启PA3口时钟和ADC3时钟，并设置PA3为模拟输入。2、复位ADC3，同时设置ADC3分频因子。3、初始化ADC3参数，设置ADC3的工作模式以及规则序列的相关信息。4、使能ADC并校准。5、配置规则通道参数。6、开启软件转换。7、等待转换完成，读取ADC值。
```
void Adc_Init(GPIO_TypeDef* GPIOx, uint16_t  GPIO_Pin_N, ADC_TypeDef* ADCx)
{
    //定义相关变量
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure; 
    
    //初始化PA3引脚
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);    //使能
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_N;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;    //设置为模拟输入模式
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;    //设置GPIO引脚速度
    GPIO_Init(GPIOx, &amp;GPIO_InitStructure);    //初始化PA3引脚
    //GPIO_SetBits(GPIOA, GPIO_Pin_3);
	
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC3 | RCC_APB2Periph_ADC2 | RCC_APB2Periph_ADC1, ENABLE);    //使能不同的ADC时钟
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);    //分频因子
    ADC_DeInit(ADCx);    //复位ADC
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;    //设置为ADC为独立模式
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;    //关闭扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;    //设置为单次转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;    //硬件触发
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;    //数据对齐方式，设置为右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 1;    //通道数量
    ADC_Init(ADCx, &amp;ADC_InitStructure);    //初始化
    ADC_Cmd(ADCx, ENABLE);

    ADC_ResetCalibration(ADCx);    //开启复位校准
    while(ADC_GetResetCalibrationStatus(ADCx));    //等待校准完成
    ADC_StartCalibration(ADCx);    //开启AD校准
    while(ADC_GetCalibrationStatus(ADCx));    //等待校准完成
}

//获取相应ADC通道采样值
uint16_t Get_Adc_Value(ADC_TypeDef* ADCx, uint8_t ch)
{
    ADC_RegularChannelConfig(ADCx, ch, 1, ADC_SampleTime_239Cycles5);    //配置规则通道
    ADC_SoftwareStartConvCmd(ADCx, ENABLE);    //开启软件转换
    while(!ADC_GetFlagStatus(ADCx, ADC_FLAG_EOC));    //等待转换完成
    return ADC_GetConversionValue(ADCx);
}

//求平均值时，可以使用一个循环语句调用Get_Adc_Value函数。
```

3、在第二步中，我们已经编写并编译好相应的读取传感器数值的程序，在这一步中我们将编译好的程序烧录到STM32开发板上。
我们将通过串口将编译好的程序烧录到STM32开发板上，首先通过USB数据线将STM32与上位机（电脑）连接。在上位机（电脑）端首先要安装CH340/CH341 USB转串口驱动，CH340驱动下载地址：http://www.wch.cn/downloads/CH341SER_EXE.html ,下载安装成功后可在设备管理器中看到相应的串口设备，如下图所示。
![](https://pad.degrowth.net/uploads/upload_cfd22f9134bfad30cb0d3f01792608bc.png)
上述步骤完成后，需要在上位机（电脑）端下载烧录工具，本次使用的烧录工具为flymcu,下载地址为：http://www.pc0359.cn/downinfo/45452.html ,下载完成后将编译好的程序烧录到STM32开发板上，flymcu使用设置如下图所示：
![](https://pad.degrowth.net/uploads/upload_5ddc6de479f7557d9b9102c6c8a79159.png)
设置好后，点击开始编程在右边显示写入成功后，我们编译好的程序就烧录到STM32开发板上了，给STM32供电后，程序就会自动运行了。在本次实验中，STM32就可以读取到传感器的采样值了。由于STM32显示设置有限，因此在这一步中我们并不能看到采集到的数值，要获取采集到的数据，让我们接着进行我们下面的实验吧。

  在STM32开发板上部署好传感器以后，传感器会实时采集周围的环境数据，此时，我们先思考一下，对于采集到的环境数据，如果我们想要将这些数据实时的展示出来，应该怎么做呢？这里有两种思路：第一种思路是在STM32上外接一个屏幕，实时的展示传感器采集到的数据。第二种思路是将STM32采集到的数据传输到另一台具有显示功能的机器上，在另一台机器上处理并展示这些传感器数据。在本次实验中，我们采用第二种思路来处理传感器采集到的数据。采用第二种思路的原因是：我们要做的是一个完整的物联网系统，对于采集到的传感器数据，我们不仅仅只是简单的显示一下就完成了，而是要对这些数据进行相应处理和存储，而STM32的存储和计算资源有限，无法满足我们的需求。因此，在本次实验中，我们将会使用树莓派来接收传感器采集到的数据。这样将会引入一个新的问题：如何将传感器的数据从STM32开发板上传输到树莓派节点上呢？最简单的方法是使用串口通信，具体的流程如下:首先将STM32通过串口与树莓派连接起来，然后STM32将采集到的数据发送到串口，树莓派再从串口读取这些数据，这样树莓派就能获取到这些数据了。当然我们还可以在STM32开发板上通过wifi模块、蓝牙模块发送数据。下面就以串口通信为例，介绍STM32如何通过串口向树莓派发送数据。

### 实验二 树莓派获取传感器数据

#### 实验目标

  本次实验的主要目标是将STM32读取到的传感器数据通过串口发送到树莓派上，要完成这一目标需要在STM32端将收集到的传感器数据发送到串口，在STM32端向串口发送数据后，在树莓派端，我们就可以读取串口来获取到这些传感器数据。在STM32端完成串口的写操作以及在树莓派端完成串口的读操作后，我们就能够在树莓派获取到这些传感器数据，从而可以对这些数据进行进一步的处理。具体的操作流程图如下：
![](https://pad.degrowth.net/uploads/upload_5eddaac5f3fddb597640993de83619b9.png)
在开始实验之前，我们首先介绍一下本次实验中涉及到的一些概念。

#### 实验原理

**串口通信**

串口通信（Serial Communications）指串口按位（bit）发送和接收字节。与串行通信相对的是并行通信。数据传输一般都是以字节传输的，一个字节8个位（bit）。拿一个并行通信举例来说，也就是会有8根线，每一根线代表一个位。一次传输就可以传一个字节，而串口通信，就是传数据只有一根线传输，一次只能传一个位，要传一个字节就需要传8次。可以看出，与并行通信相比，串行通信占用的硬件资源较少，相应的传输速度较慢一些。

典型的串口通信使用3根线完成，分别是地线、发送线、接收线。串口通信中最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通行的端口，这些参数必须匹配。波特率为每秒传输的bit个数，用来衡量传输速率。奇偶校验位用来验证接收到的数据是否是正确的。数据位是衡量通信中实际数据位的参数。停止位用于表示单个数据包的最后一位。

在STM32中，串口通信设备为USART(通用同步异步收发器),通用同步异步收发器(USART)提供了一种灵活的方法与使用工业标准NRZ(不归零编码)异步串行数据格式的外部设备之间进行全双工数据交换。其接口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚：接收数据输入(RX)和发送数据输出(TX)。
RX：接收数据串行输。通过过采样技术来区别数据和噪音，从而恢复数据。
TX：发送数据输出。当发送器被禁止时，输出引脚恢复到它的I/O端口配置。当发送器被激活，并且不发送数据时，TX引脚处于高电平。在单线和智能卡模式里，此I/O口被同时用于数据的发送和接收。

**中断**

中断是用以提高计算机工作效率、增强计算机功能的一项重要技术。最初引入硬件中断，只是出于性能上的考量。如果计算机系统没有中断，则处理器与外部设备通信时，它必须在向该设备发出指令后进行忙等待（Busy waiting），反复轮询该设备是否完成了动作并返回结果。这就造成了大量处理器周期被浪费。引入中断以后，当处理器发出设备请求后就可以立即返回以处理其他任务，而当设备完成动作后，发送中断信号给处理器，后者就可以再回过头获取处理结果。这样，在设备进行处理的周期内，处理器可以执行其他一些有意义的工作，而只付出一些很小的切换所引发的时间代价。后来中断被用于CPU外部与内部紧急事件的处理、机器故障的处理、时间控制等多个方面，并产生通过软件方式进入中断处理（软中断）的概念。

中断是指在程序执行过程中，出现某种紧急事件，CPU暂停执行现行程序，转去执行处理该事件的程序--中断服务程序，执行完后再返回到被暂停的程序继续执行，这一过程称为中断。
![](https://pad.degrowth.net/uploads/upload_1df3c2eb222b9f0503d6a366ee61b2e0.png)
中断处理的基本过程一般包括中断请求、中断判优、中断响应、中断服务和中断返回五个阶段。

#### 实验步骤

1、STM32写串口

STM32写串口的过程比较简单，首先要初始化要使用的串口引脚，设置波特率、数据位、停止位和奇偶校验等参数，然后可以调用库函数USART_SendData()来向串口发送数据，USART_SendData()函数每次向串口发送一个字符，要想向串口发送字符串，需要多次调用该函数。
首先我们查看STM32F103系列原理图（如下图）确定要用到的引脚，在本次实验中，我们使用PA9和PA10引脚，因为本次使用的STM32开发板中有USB接口转串口TTL模块，使用这两个引脚只需要用一根USB数据线即可将与树莓派通过串口连接起来，不需要额外的线来连接。
![](https://pad.degrowth.net/uploads/upload_f5270c26e54b570b4992e91caab92ece.png)

```
void Usart1_Init(void)
{
    //中断控制初始化结构体
    NVIC_InitTypeDef NVIC_InitStructure;
    GPIO_InitTypeDef gpio_init;
    USART_InitTypeDef usartStruct;
	
    //1.配置时钟：GPIO时钟，复用时钟，串口时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	
    //2.配置GPIO的结构体
    //初始化发送引脚PA9 TX
    gpio_init.GPIO_Mode = GPIO_Mode_AF_PP;    //GPIO_Mode设置为复用推挽模式
    gpio_init.GPIO_Pin = GPIO_Pin_9;  
    gpio_init.GPIO_Speed = GPIO_Speed_10MHz;    //设置GPIO引脚速度
    GPIO_Init(GPIOA,&amp;gpio_init);    //初始化操作
	
    //初始化接收引脚PA10 RX
    gpio_init.GPIO_Mode = GPIO_Mode_IN_FLOATING;    //GPIO_Mode设置为浮空输入模式
    gpio_init.GPIO_Pin = GPIO_Pin_10;
    gpio_init.GPIO_Speed = GPIO_Speed_10MHz;    //设置GPIO引脚速度
	
    GPIO_Init(GPIOA,&amp;gpio_init);    //初始化操作
	
    //3.配置串口
    usartStruct.USART_BaudRate = 9600;    //设置波特率
    usartStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;    //不使用硬件流控制
    usartStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;    //使能发送和接收
    usartStruct.USART_Parity = USART_Parity_No;    //不使用奇偶校验位
    usartStruct.USART_StopBits = USART_StopBits_1;    //设置停止位
    usartStruct.USART_WordLength = USART_WordLength_8b;    //设置数据位
    USART_Init(USART1, &amp;usartStruct);    //初始化串口1
	
    /*
    * NVIC属于M3内核的一个外设，控制着芯片的中断相关功能。
    * STM32F103芯片支持60个可屏蔽中断通道，每个中断通道都具备
    * 自己的中断优先级控制字节(8位，但是STM32F103中只使用4位
    * ，高4位有效)，用于表达优先级的高4位组成抢占式优先级
    * 和响应优先级，每个中断源都需要被指定这两种优先级。
    */
    //Usart1 NVIC（嵌套向量中断控制器）设置

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //设置NVIC中断分组
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;    //设置中断源
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;    //设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;    //设置响应优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    //IRQ通道使能
    NVIC_Init(&amp;NVIC_InitStructure);    //初始化操作
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);    //使串口能接收中断
    USART_Cmd(USART1, ENABLE);    //使能串口
}

//发送一个字符到串口
void Usart1_Send_Byte(USART_TypeDef* USARTx, uint16_t Data)
{
    //发送字符到串口
    USART_SendData(USARTx,Data);
    // 等待发送数据寄存器为空, 防止后来的数据把前面的数据覆盖
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}

//重定向c库函数printf到串口，在调用printf函数时，输出数据到串口
int fputc(int ch, FILE *f)
{
    // 发送一个字节数据到串口
    USART_SendData(USART1, (uint8_t) ch);
    // 等待发送完毕，等待发送缓冲区为空
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);       
    return (ch);
}
```
我们可以调用Usart1_Send_Byte()函数一次发送一个字符到串口，当然对于我们采集到的传感器数据，一次发送一个字符操作过于繁琐，因此我们可以直接使用printf()函数发送一个字符串到串口。发送数据的格式为“传感器名:数值”，每条数据占一行，以‘\n’结尾。调用示例：printf(&#34;Light:%.2f\n&#34;, value);
有些时候，我们需要调试我们的程序，为了方便调试，我们可以在上位机（电脑）上安装串口调试助手来帮助我们查看上位机（电脑）端串口接收到的数据，在win10系统下有很多的串口调试工具，本次实验中，我们使用的sscom串口调试工具，下载地址为：http://www.sscom.vip/ ，下载安装成功后，我们可以使用该工具方便的调试我们的程序。

2、树莓派读串口数据

为了能正确读取传感器采集到的数据，我们需要规定一下数据的收发格式，本次实验中，我们规定STM32端发送的数据格式为“传感器名:数值”，每条数据占一行，以‘\n’结尾。因此在树莓派端，我们可以按行读取每一条传感器数据。在树莓派上，使用python语言读取串口是很方便的，python提供了一个pyserial库可以非常方便的读写串口。读取串口的一般流程为：1、打开要读取的串口并设置相应的波特率等参数。2、按行读取串口上的数据。3、显示读取到的数据。具体操作如下。
首先在树莓派上安装pyserial库：
```shell=
sudo apt install python-pip #安装pip工具
pip install pyserial
```
安装成功后我们可以编写相应的代码，关键代码如下：
```python
ser = serial.Serial(&#39;/dev/ttyUSB0&#39;,&#39;9600&#39;)    #打开串口，视情况填写串口号这一参数
if ser.isOpen():
    print(&#39;串口打开成功！\n&#39;)
else :
    print(&#39;串口打开失败！\n&#39;)
try:
    while True:
        data = ser.read_until(b&#39;\n&#39;).decode(&#39;utf-8&#39;)    #按行读取串口数据
        print(data, end=&#39;&#39;)    #输出数据
except KeyboardInterrupt:
    if ser != None:
        ser.close()    #关闭串口
```

本次实验中我们读取到的数据如下：
![](https://pad.degrowth.net/uploads/upload_32f9cc67648eac790ca798a15c45f94e.png)
可以看到，采集到的数据除了Temperature为实际的温度数值外，其余三个传感器数据不是实际周围环境数据的具体数值，而是其ADC通道采样的平均值。需要根据相应的公式进行转换。要根据ADC通道采样平均值计算出相应的属性强度，具体的计算公式如下：result = 100 – (ADCValue / 40)，ADCValue为采集到的数值，其取值范围为0~100，数值越高表示相关属性越强。如果ADCValue大于4000，取ADCValue=4000。根据上述公式转换后，我们就可以获取到周围环境数据的具体数值。

完成上面的两个步骤后，我们就可以在树莓派上获取到传感器收集到的环境数据了。上述操作相当于是STM32通过串口向树莓派发送数据，树莓派接收数据。有些时候，我们还想要使用树莓派向STM32发送一些控制信息来控制连接到STM32上的传感器工作状态、采样频率等等。这时候，就需要我们在STM32端读取串口上的数据，在树莓派端发送控制信息数据到串口。接下来我们就介绍一下如何实现上述的操作。

### 实验三 树莓派发送数据到STM32

#### 实验目标

在这个实验中，我们要实现的目标是，在STM32端，我们要读取串口上的数据。在树莓派端，我们要向串口发送数据。最终我们可以实现从树莓派端向STM32端发送数据，STM32端接收到树莓派发来的数据。

#### 实验步骤

1、STM32读串口
STM32读串口首先也要初始化的串口引脚以及USART模块，当STM32串口接收到一个字符时，会产生一个中断，我们要自定义相应的中断处理函数来处理串口接收到的数据。初始化操作与实验二中的STM32写串口操作一样，这里不再进行说明。下面主要介绍串口中断服务函数。
```
//定义缓存数组及一些标志信息，为全局变量
char RxBuffer[RxBufferSize];   //串口接收缓冲数组
uint8_t RxCounter = 0;                  //串口接收缓冲数组索引
uint8_t Status_RxFrame = 0;         //串口帧接收完成标志位
uint8_t Status_RxBuffer_Overflow = 0;       //串口接收缓冲溢出标志位，1-溢出，需进行清空缓冲数组，并发送调试信息

//串口中断服务函数，处理串口收到的数据
void USART1_IRQHandler(void)
{
    //当串口发生中断时，将接收到的字节保存到缓冲数组中
    if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET)
    {       
        if(RxCounter &lt; RxBufferSize &amp;&amp; Status_RxBuffer_Overflow != 1)    //缓冲数组未溢出
        {
            RxBuffer[RxCounter++] = (char)USART_ReceiveData(USART1);    //将接收到的字符存储到缓冲数组中
            //这条if语句判断是否接收完一个字符串
            if(RxCounter &gt; 1)    
            {
                //这里我们规定‘#’为一个数据帧的结束符
                if(RxBuffer[RxCounter-1] == &#39;#&#39;)
                {
                    Status_RxFrame = 1;    //串口帧接收完成
                    RxCounter = 0;    //重置数组索引
                    USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);    //失能串口中断，防止数据覆盖
                }
            }                                        
        }
        else    //缓冲数组中的数据溢出处理
        {                                                  
            USART_ITConfig(USART1, USART_IT_RXNE, DISABLE);  //失能串口中断
            Status_RxBuffer_Overflow = 1;    //溢出标志位置1
            RxCounter = 0;    //缓冲数组索引重置
        }  
    }
} 

//主函数中接收串口数据相应的操作
if(Status_RxBuffer_Overflow != 1)    //接收串口数据
{
    if(Status_RxFrame == 1)
    {
        printf(&#34;text: %s\n&#34;, RxBuffer);	   //打印串口接收到的数据
        Status_RxFrame = 0;    //数据帧处理完毕后，将帧完成标志位清零
        for(i=0; i&lt;RxBufferSize; i++)    //将缓冲数组清零
        {
            RxBuffer[i] = 0;
        }
        USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);    //重新启用接收中断，以便接收下一个数据帧
    }
}
```
到这里，我们就可以STM32串口中的数据了。STM32串口的数据来源是哪里呢，在我们的物联网系统中，会使用树莓派向STM32串口发送数据。下面我们介绍树莓派写串口的操作。

2、树莓派写串口

在树莓派上，使用python语言向串口写数据同样用到了pyserial库，写串口的一般流程为：1、打开要读取的串口并设置相应的波特率等参数。2、调用write函数向串口中写数据。相关代码如下：
```python=
#打开相应串口并设置相关参数
ser = serial.Serial(&#39;/dev/ttyUSB0&#39;, &#39;9600&#39;, timeout=1, bytesize=8, parity=&#39;N&#39;, stopbits=1)
if ser.isOpen():    #判断串口是否打开成功
    print(&#39;串口打开成功！\n&#39;)
else:
    print(&#39;串口打开失败！\n&#39;)
try:
    while True:
        str1 = input(&#34;请输入要发送到串口的数据：&#34;)
        result = ser.write((str1).encode(&#39;utf-8&#39;))  #向串口写入数据，该函数返回写入字符串个数
        sleep(1)
except KeyboardInterrupt:
    ser.close()
```
上述步骤在树莓派端向STM32发送数据，想要在STM32端查看树莓派发来的数据，需要在STM32端调用读串口的程序，然后再发送到上位机（电脑/树莓派）上查看收到的数据。

通实验二和实验三实验我们已经可以在树莓派上获取到传感器采集到的周围环境数据了，并且还可以通过树莓派向STM32发送一些控制信息。在下面的一个实验中，我们将介绍在STM32端如何使用DMA方式来读写串口。我们知道使用中断方式已经可以完成STM32串口读写的功能了，为什么还要再引入DMA方式的串口读写呢？为了说明这一问题，我们首先简要介绍一下DMA，DMA全称Direct Memory Access(直接存储器访问)，是一种内存访问技术，它允许外设与存储器之间或则存储器与存储器之间直接交互数据，而不需要中央处理器(CPU)介入处理。这样做大大减少了CPU因处理数据读写操作而产生中断次数，我们知道在中断期间，CPU是无法处理其它任务的，减少CPU中断次数，可以让CPU有更多时间去处理其它的任务，从而提高了CPU的资源利用率。因此，使用DMA方式读写串口可以减少CPU的负载，提高CPU的利用率。下面就来介绍使用DMA方式来读写STM32串口。

### 实验四 STM32串口读写(DMA方式)

#### 实验目标

本次实验的主要目标是在STM32端，使用DMA方式读取串口中的数据，或者向串口中写入数据。从我们整个物联网系统来看，本次实验实现的功能和实验二实验的功能是一样的。与实验二不同的是，本次实验在读写STM32串口时，引入了DMA，提高CPU资源利用率。

#### 实验原理

**DMA介绍**
DMA，全称Direct Memory Access，直接存储器访问。它允许不同速度的硬件设备来沟通，而不需要依赖于中央处理器（CPU）的大量中断负载。DMA传输将数据从一个地址空间复制到另一个地址空间，提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。例如，从硬盘移动数据到内存中。
在实现DMA传输时，是由DMA控制器直接掌管总线，因此，存在着一个总线控制权转移问题。即DMA传输前，CPU要把总线控制权交给DMA控制器，而在结束DMA传输后，DMA控制器应立即把总线控制权再交回给CPU。
![](https://pad.degrowth.net/uploads/upload_aa5457ee87d73a8f0157b7c87f42207e.jpg)
DMA的一般有以下四个步骤：
DMA请求，CPU初始化DMA控制器，外设（I/O接口）发出DMA请求。
DMA响应，DMA控制器判断DMA请求的优先级及屏蔽，向总线仲裁器提出总线请求。当CPU执行完当前总线周期即可释放总线控制权。此时，总线裁决逻辑输出总线应答，表示DMA已经响应，通过DMA控制器通知I/O接口开始DMA传输。
DMA传输，DMA控制器获得总线控制权后，CPU开始挂起或只执行内部操作，由DMA控制器输出读写命令，直接控制RAM与I/O接口进行DMA传输。DMA数据以规定的传输单位（通常是字）传输，每个单位的数据传送完成后，DMA控制器修改地址，并对传送单位的个数进行计数，继而开始下一个单位数据的传送，如此循环往复，直至达到预先设定的传送单位数量为止。
DMA结束，当DMA数据传输完成后，DMA控制器通知外设（I/O接口）停止传输，并向CPU发送一个信号（产生中断或事件）报告DMA数据传输操作结束，同时释放总线控制权。
可以看出，使用DMA方式传输数据能够提高CPU利用率以及数据传输效率。因此在现代计算机中被广泛使用。但是DMA也引入了新的问题，缓存一致性问题，考虑中央处理器带有缓存与外部内存的情况，DMA的运作则是去访问外部内存，当中央处理器访问外部内存某个地址的时候，暂时先将新的值写入缓存中，但并未将外部内存的数据更新，若在缓存中的数据尚未更新到外部内存之前发生了DMA，则DMA过程将会读取到未更新的数据。

STM32F103开发板上有两个DMA控制器，共12个通道（DMA1有7个通道，DMA2有5个通道），每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。每个通道都同样支持软件触发。这些功能通过软件来配置。在同一个DMA模块上，多个请求间的优先权可以通过软件编程设置(共有四级：很高、高、中等和低)，优先权设置相等时由硬件决定(请求0优先于请求1，依此类推) 。

#### 实验步骤

串口使用DMA方式发送和接收数据分为以下几步：1、初始化串口。2、初始化DMA。3、发送数据到串口。4、从串口读取数据。
1、初始化串口，初始化串口的过程与本章实验二中初始化过程相同，可按照实验二中的串口初始化流程进行初始化。与实验二中初始化过程不同的一点需要使能串口的DMA发送和接收功能，使能语句如下：
```
    USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE);  //使能串口1的DMA发送     
    USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE);  //使能串口1的DMA接收  
```
2、DMA初始化，查看各个通道的DMA1请求表（如下表），可以看出USART1_TX与USART2_RX的DMA1请求分别在通道4和通道5上。要实现串口DMA收发数据需要对通道4和通道5分别进行配置，配置的参数包括内存地址，外设地址，传输数据长度，数据宽度，通道优先级。下面在初始化代码中介绍每个参数的设置。
![](https://pad.degrowth.net/uploads/upload_71316f92043a5f9e9cece1bf88b196a6.png)

```
#define RECEIVE_BUF_SIZE 255
#define SEND_BUF_SIZE 255
//发送或接收过程缓冲区定义，全局变量
uint8_t  g_uart1ReceiveBuff[RECEIVE_BUF_SIZE];   //接收缓冲  
uint8_t  g_uart1SendBuff[SEND_BUF_SIZE];    //发送数据缓冲区  
uint16_t g_uart1ReceiveSize = 0;  

uint8_t  g_uart3ReceiveBuff[RECEIVE_BUF_SIZE];   //接收缓冲  
uint8_t  g_uart3SendBuff[SEND_BUF_SIZE];    //发送数据缓冲区  
uint16_t g_uart3ReceiveSize = 0;

void DMA_Config(void)
{
    DMA_InitTypeDef  DMA_InitStructure;  //定义相关变量
    NVIC_InitTypeDef NVIC_InitStructure;
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);	//时钟配置
	
    /****************************配置USART1发送***********************/
    DMA_DeInit(DMA1_Channel4);  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&amp;USART1-&gt;DR;//DMA外设地址  
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)g_uart1SendBuff;//DMA 存储器地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;//存储器到外设模式  
    DMA_InitStructure.DMA_BufferSize = SEND_BUF_SIZE;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级  
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  //DMA通道x没有设置为内存到内存传输
    DMA_Init(DMA1_Channel4, &amp;DMA_InitStructure);//初始化DMA
    DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE);
	
    /***************************配置USART1接收***********************/
    DMA_DeInit(DMA1_Channel5);  
    DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&amp;USART1-&gt;DR;//DMA外设地址  
    DMA_InitStructure.DMA_MemoryBaseAddr = (u32)g_uart1ReceiveBuff;//DMA 存储器地址  
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC ;//外设到存储器模式  
    DMA_InitStructure.DMA_BufferSize = RECEIVE_BUF_SIZE;//数据传输量   
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设非增量模式  
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器增量模式  
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据长度:8位  
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器数据长度:8位  
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;// 使用普通模式   
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//中等优先级  
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  //DMA通道x没有设置为内存到内存传输
    DMA_Init(DMA1_Channel5, &amp;DMA_InitStructure);//初始化DMA Stream  
    DMA_ITConfig(DMA1_Channel5,DMA_IT_TC,ENABLE);  
	
    /***************************配置NVIC***********************/
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);    //嵌套向量中断控制器组选择
    // 配置USART1发送  
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn; //设置中断源   
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  //设置抢占优先级
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //设置响应优先级
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  //IRQ通道使能  
    NVIC_Init(&amp;NVIC_InitStructure);   //初始化操作 
	
    // 配置USART1接收 
    NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel5_IRQn;    
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;    
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;    
    NVIC_Init(&amp;NVIC_InitStructure); 
    DMA_Cmd(DMA1_Channel5, ENABLE);  //开启DMA传输
}
```
在初始化串口和DMA后，我们就可以开始定义相关的发送和接收函数来向串口发送数据或者从串口读取数据。

3、发送数据到串口，在我们进行DMA配置以及使能串口DMA发送功能之后，我们可以使能DMA传输通道来开启DMA传输。相应的发送函数如下：
```
/**
 @brief 开启一次DMA传输 
 @param DMA_Streamx DMA数据流,通道
 @param ndtr 数据传输量
 @return 无
*/
void DmaSendDataProc(DMA_Channel_TypeDef *DMA_Streamx,uint16_t ndtr)  
{  

    DMA_Cmd(DMA_Streamx, DISABLE);    //关闭DMA传输   
    DMA_SetCurrDataCounter(DMA_Streamx,ndtr);     //指定数据传输量 
    DMA_Cmd(DMA_Streamx, ENABLE);     //使能DMA传输通道，开启DMA传输   
}
/**
 @brief 串口1使用DMA发送多字节,从发送缓存区中发送数据
 @param pSendInfo 要发送的数据
 @param nSendCount 发送数据的长度
 @return 无
*/ 
void Uart1DmaSendString(char* pSendInfo, uint16_t nSendCount)  
{  
    uint16_t i = 0;  
    uint8_t *pBuf = NULL;  
    //指向发送缓冲区  
    pBuf = g_uart1SendBuff;
    //将要发送的数据复制到发送缓冲区中。
    for (i=0; i&lt;nSendCount; i++)  
    {  
        *pBuf++ = pSendInfo[i];  
    }  
    //DMA发送方式  
    DmaSendDataProc(DMA1_Channel4, nSendCount); //开始一次DMA传输   
}  
```
当串口使用DMA发送完成一次数据后，会发送一次中断，因此我们需要定义相应的中断函数。
```
void DMA1_Channel4_IRQHandler(void)  
{  
    //清除标志  
    if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET)//等待传输完成  
    {   
        DMA_ClearFlag(DMA1_FLAG_TC4);//清除传输完成标志  
    }  
} 
```

4、读取串口数据，当一次DMA数据传输完成后，会产生中断，我们可以在中断函数中获取到一次接收的数据。
```
void DMA1_Channel5_IRQHandler(void)  
{  
    //清除标志  
    if(DMA_GetFlagStatus(DMA1_FLAG_TC5)!=RESET)//等待传输完成  
    {   
        DMA_Cmd(DMA1_Channel5, DISABLE); //关闭DMA,防止处理其间有数据  
			  
        g_uart1ReceiveSize =RECEIVE_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);  
        if(g_uart1ReceiveSize !=0)  
        {  
            printf(&#34;receive:%s&#34;, g_uart1ReceiveBuff);    //需要重定向printf函数到串口才能在上位机上查看串口数据。
        }  
        DMA_ClearFlag(DMA1_FLAG_TC5 | DMA1_FLAG_TE5 | DMA1_FLAG_HT5);//清除DMA1传输完成标志  
        DMA_SetCurrDataCounter(DMA1_Channel5, RECEIVE_BUF_SIZE);    //
        DMA_Cmd(DMA1_Channel5, ENABLE);     //使能DMA通道 
    }  
} 
```
完成以上的步骤我们就可以使用DMA方式在STM32上读写串口了。

### 实验五 使用DMA与不使用DMA读写串口对比实验

通过第一章的实验，我们已经可以使用树莓派和STM32获取传感器采集到的环境数据。这是整个物联网系统的数据来源。在我们的物联网系统中，每一个节点都配备了一个树莓派和一个STM32开发板，我们将使用很多这样的节点来采集不同位置的环境数据。做为一个物联网系统，各个节点之间肯定不会是相互独立的，而是可以相互通信的。因此，在配置好单个节点后，我们需要将我们配置好的节点连接起来组成一个小型网络，并且处于这个网络中的各个节点能够相互通信。在下一个章节的实验中，我们将介绍如何使用路由器来搭建一个小型的局域网络，其中我们不使用专用的路由器，而是配置我们手中树莓派，使其具有简单的路由器的功能。

## 第二章 小型局域网搭建与RIP/OSPF路由

通过第一章的实验，我们配置好的单个节点已经具备采集周围环境数据的功能了，现在，我们的物联网系统中将会有很多这样的节点，在这一章中，我们将介绍如何使用路由器将这些节点连通起来，搭建出一个小型的局域网络。搭建好网络后，我们将会在路由器上部署我们在《计算机网络》这门课程中学过的一些路由协议，使得处在不同网段中的节点（或者说是连接在不同路由器上的节点）之间能够相互通信。下面就让我们从搭建一个小型局域网开始我们的实验吧。

### 实验一 小型局域网的搭建
#### 实验目标
本次实验的目标是使用两个树莓派节点做为路由器搭建一个传统的局域网络。网络拓扑图如下所示：
![](https://pad.degrowth.net/uploads/upload_9e5b2f6a9467af7bedf2ffa1cd59c02b.jpg)

  搭建好网络以后,因为还没有部署相应的路由协议,因此我们将手动指定路由器中的默认网关,使得处于192.168.100.0/24网段中的设备可以和处于192.168.110.0/24网段中的设备相互通信。

注意：这一个实验完成后，我们的树莓派将无法联网，但是后续的实验需要联网安装一些工具包，因此建议在安装完后续实验所用到的工具包之后再开始进行本实验。所需安装工具包在后续实验中有提到，可以在后面的实验中查看需要安装哪些工具包。

#### 实验步骤
  在本次实验中，我们需要使用两个路由器节点，如果我们手中没有专用的路由器来进行本次实验的话，我们可以修改树莓派上的无线网卡配置来将树莓派改造成一个具有路由器功能的节点。无线网卡一般有四种工作模式：Managed，Master，Ad-hoc，Monitor。Managed模式用于无线客户端直接与无线接入点（Access Point，即AP）进行接入连接，默认情况下我们的树莓派无线网卡是工作在这个模式下的。在Master模式下允许无线网卡使用特制的驱动程序和软件工作，作为其他设备的无线AP。它主要使用于无线接入点AP提供无线接入服务以及路由功能。在这个实验中我们树莓派的无线网卡就是工作在这个模式下的。在Ad-hoc模式下，各设备之间采用对等网络的方式进行连接，无线通信双方共同承担无线AP的职责。这个模式我们之前的实验中有使用过。Monitor模式主要用于监控无线网络内部的流量，用于检查网络和排错。本实验教程中并未使用到该模式，感兴趣的同学可以自行尝试配置此模式，了解无线网卡在该模式下的特点。

  树莓派上一般有以太网接口和无线网卡。通过前面的介绍我们可以知道，只要将树莓派上的无线网卡设置为Master（AP）模式，这样其他联网设备可以通过WiFi连接到该树莓派，树莓派就具有路由器的基本功能了。
  配置好无线网卡后，我们再使用一根网线将两个树莓派通过以太网接口连接起来，使得两个树莓派相互连通。为了方便管理，我们将这些网络接口的IP地址都设置为静态IP。具体的IP地址已经在网络拓扑图中展示出来了。
  首先我们将树莓派无线网卡配置为AP模式。一个WiFi热点（Access Point）正常工作基本要实现两部分功能：实现接入管理和DHCP功能。接入管理主要实现的是设置WiFi名称并将其广播出去，使得其它设备可以接收到这个热点。DHCP功能主要为接入的新设备分配一个IP地址，使其能够正常通信。下面我们首先实现接入管理。

  在本次实验中，我们使用hostapd来实现接入管理，Hostapd（Host access point daemon）是一个用户空间的守护进程，主要用于无线接入点（AP）和授权服务器上。它实现了IEEE802.11接入结点管理，IEEE802.1X/WPA/WPA2/EAP认证以及RADIUS客户端、EAP服务器和RADIUS认证服务器的功能。使用Hostapd能够是我们的无线网卡切换为Master模式，使其模拟为一个路由器。
首先我们在树莓派上安装hostapd，命令如下：

```shell
sudo apt-get install hostapd
```

  安装完成后要修改hostapd的配置文件，文件位置在/etc/hostapd/hsotapd.conf，如果文件不存在则创建文件。文件中的具体内容如下：

```shell
interface=wlan0  #选择网络接口名称，可以使用ifconfig命令查看可用接口
driver=nl80211  #驱动选择，一般有两种：wext/nl80211，wext版本较旧目前一般使用nl80211
ssid=APname  #AP名称
hw_mode=g    #802.11g,一般三个模式: a,b,g
channel=6    #工作的信道
ieee80211n=1  #iEEE802.11n
wmm_enabled=1  #启用WMM
wpa=2  # 1 wpa，2 wpa2, 3 两者。指定wpa版本
wpa_key_mgmt=WPA-PSK  #加密方式
wpa_passphrase=raspberry  #密码
rsn_pairwise=CCMP  #加密算法
```

  上述配置文件中的信息即为要创建热点的基本信息。其中ssid为热点名称，wpa_passphrase为热点密码。可以根据需要修改这两个参数。

  之后要向hostapd指定它配置文件的位置，修改文件/etc/default/hostapd,添加下面语句：

```shell
DAEMON_CONF=&#34;/etc/hostapd/hostapd.conf&#34;
```

  配置好hostapd后，我们接着实现DHCP功能，本次实验使用dnsmasq工具来实现DHCP功能。Dnsmasq 提供 DNS 缓存和 DHCP 服务功能。作为域名解析服务器(DNS)，dnsmasq可以通过缓存 DNS 请求来提高对访问过的网址的连接速度。作为DHCP 服务器，dnsmasq 可以用于为局域网电脑分配内网ip地址和提供路由。
  dnamasq具体安装配置过程如下：

```shell
sudo apt-get install dnsmasq
```

  安装完成后修改相应的配置文件/etc/dnsmasq.conf。

```shell
interface=wlan0
listen-address=192.168.100.1
bind-interfaces 
server=8.8.8.8
domain-needed
bogus-priv
dhcp-range=192.168.100.100,192.168.100.200,24h
```

  其中interface为要监听的接口，listen-address为要接口对应的IP地址，本次实验中为是wlan0接口的静态IP地址，设置静态IP地址的方法将在下面的内容中介绍。server为DNS服务器地址，8.8.8.8是谷歌的默认DNS服务器地址（本次实验中不会用到）。dhcp-range为给接入设备分配的IP地址范围。其中两个树莓派配置文件不同，树莓派Pi1的listen-address为192.168.100.1，dhcp-range为192.168.100.100,192.168.100.200。树莓派Pi2的listen-address为192.168.110.1，dhcp-range为192.168.110.100,192.168.110.200。

  设置hostapd和dnsmasq开机自动启动：

```shell
#设置hostapd开机自启
systemctl unmask hostapd.service
systemctl enable hostapd.service
#设置dnsmasq开机自启
systemctl enable dnsmasq.service
```

  hostapd和dnsmasq配置完成后暂时先不要启动，因为启动以后树莓派将会无法上网，如果你是通过ssh连接树莓派的话，那么启动后将无法再连接到树莓派，因此我们要先设置静态IP，后面可以通过网线与树莓派连接。

  设置静态IP的方法是在修改树莓派中的/etc/network/interfaces，其中树莓派Pi1中的/etc/network/interfaces文件具体内容如下：

```shell
auto wlan0
allow-hotplug wlan0
iface wlan0 inet static #静态ip
address 192.168.100.1  #静态ip地址
netmask 255.255.255.0  #子网掩码
network 192.168.100.0  #网段
broadcast 192.168.110.255  #广播地址

auto eth0
iface eth0 inet static
address 192.168.101.10
netmask 255.255.255.0
network 192.168.101.0
gateway 192.168.101.20
broadcast 192.168.101.255
```

树莓派Pi2中的/etc/network/interfaces文件具体内容如下：

```
auto wlan0
allow-hotplug wlan0
iface wlan0 inet static
address 192.168.110.1
netmask 255.255.255.0
network 192.168.110.0
broadcast 192.168.110.255

auto eth0
iface eth0 inet static
address 192.168.101.20
netmask 255.255.255.0
network 192.168.101.0
gateway 192.168.101.10
broadcast 192.168.101.255
```

上述步骤完成后，可以用电脑连接树莓派的热点，然后使用ssh访问树莓派，访问到树莓派后，在树莓派Pi1上使用ping 192.168.110.1命令或者在树莓派Pi2上使用ping 192.168.100.1命令检测两个不同网段是否连通。另外，可以在树莓派上使用route命令查看相应的路由表。

网络搭建好后，我们有时候想要查看在网络中传输的数据包的情况，这个时候就需要我们在网络中进行抓包了，下面我们将介绍如何使用tcpdump工具在linux环境下进行抓包分析网络中的数据包。


### 实验二RIP/OSPF路由协议部署

#### 实验目标

在实验一中，我们搭建了一个小型的局域网络。并且在两个路由器（树莓派）eth0接口上指定了网关使得两个网段中的网络可以连通。在本次实验中，我们将不在路由器（树莓派）上指定网关，而是分别在两个路由器上部署RIP/OSPF路由协议，根据路由协议来生成路由表，使得实验一网络拓扑中不同网段的网络能够连通。

#### 实验步骤

首先我们要在树莓派（路由器）上修改实验一中已经配置过的/etc/network/interfaces文件，删除掉文件中指定网关的语句，例如gateway 192.168.101.10，在两个树莓派（路由器）上都删除掉该语句后，使用/etc/init.d/networking restart命令重启网络。重启完成后，实验一中搭建的处于两个网段中的网络将不再连通。使用ping命令将无法互相ping通。接下来我们要在树莓派（路由器）上部署路由协议，使得这两个网段中的网络能够相互连通。

本次实验中使用quagga在树莓派（路由器）上部署RIP或者OSPF路由协议，Quagga 是一个支持 OSPFv2、OSPFv3、RIPv1 和 v2、RIPng、以及 BGP-4 的路由协议套件。想要了解更多关于quagga的信息可以查询quagga的项目网站：https://quagga.net/ quagga源码地址为：https://github.com/Quagga/quagga 首先我们在树莓派（路由器）上安装quagga。安装命令如下：

```shell
sudo apt-get install quagga
```

顺利执行完命令没有报错的话，安装就基本完成了。安装成功后可以在/etc/services文件夹下看到相关协议对应的端口号，如下图所示：
![](https://pad.degrowth.net/uploads/upload_5f619b9bfd209f96bd71fbe25278b46d.png)

安装完成后接下来要修改相关的配置文件。首先复制示例配置文件xxx.conf.sample到树莓派的/etc/quagga/文件夹下，示例配置文件在quagga项目源码文件夹里可以找到。其中最基本的为zebra.conf文件，树莓派Pi1中的zebra.conf配置文件如下：

```shell
! -*- zebra -*-
!
! zebra sample configuration file
!
! $Id: zebra.conf.sample,v 1.1 2002/12/13 20:15:30 paul Exp $
!
hostname Router
password zebra
enable password zebra
!
! Interface&#39;s description.
!
interface eth0
  ip address 192.168.101.10/24
! description test of desc.
!
interface wlan0
  ip address 192.168.100.1/24
! multicast
!
! Static default route sample.
!
!ip route 0.0.0.0/0 203.181.89.241
!
log file /var/log/quagga/zebra.log
```

注：文件中的感叹号”!“表示注释

树莓派Pi1中的/etc/quagga/ripd.conf文件内容如下：

```shell
! -*- rip -*-
!
! RIPd sample configuration file
!
! $Id: ripd.conf.sample,v 1.1 2002/12/13 20:15:30 paul Exp $
!
hostname RouterPi_2
password zebra
!
 debug rip events
 debug rip packet
!
router rip
 network 192.168.101.0/24
 network 192.168.110.0/24
! network eth0
! route 10.0.0.0/8
! distribute-list private-only in eth0
!
!access-list private-only permit 10.0.0.0/8
!access-list private-only deny any
!
log file /var/log/quagga/ripd.log
!
log stdout
```

树莓派Pi1中的/etc/quagga/ospfd.conf文件内容如下：

```
! -*- ospf -*-
!
! OSPFd sample configuration file
!
!
hostname ospfd
password zebra
enable password zebra
!enable password please-set-at-here
!
router ospf
  network 192.168.101.0/24 area 0
  network 192.168.110.0/24 area 0
!
log file /var/log/quagga/ospfd.log
log stdout
```

树莓派Pi1中的配置文件配置好以后，可以参考树莓派Pi1中的配置文件修改树莓派Pi2中相关文件。

配置完成后可以通过service命令启动zebra、ospfd或者ripd程序，具体命令如下：

```shell
sudo service zebra start
sudo service ospfd start/sudo service ripd start
```

其中，zebra程序是基本程序首先要启动，ospfd和ripd根据需要选择其中一个启动即可。

默认情况下，安装quagga成功后，所有有关quagga的程序都会开机自启，可以使用systemctl命令管理开机自动启动项，具体命令如下：

```shell
sudo systemctl disable ripd.service    #禁止开机自启ripd程序
sudo systemctl disable ospfd.service    #禁止开机自启ospfd程序
```

建议首先运行上述两个命令先禁止ripd以及ospfd程序自动启动，否则重启树莓派后可能会同时运行多个路由协议，本次实验建议在路由器上运行单个的路由协议。

在上述设置都配置好以后，重启两个树莓派（路由器），重启完成后就可以开始配置ospf/rip协议，本教程以启动ospf协议为例，使用如下命令来启动ospf程序。

```shell
sudo service zebra start
sudo service ospfd start
```

启动成功后，可以在任意一个树莓派（路由器）上使用sudo vtysh命令进入路由器虚拟管理界面，进入路由器虚拟管理界面以后可以使用show ip ospf neighbor命令查看ospf路由查找到的邻居节点，结果如下：

![](https://pad.degrowth.net/uploads/upload_eb748c90a8f1cc765e6ff4f5d8da75c2.png)

使用show ip route命令查看ospf协议生成的路由表信息，结果如下：

![](https://pad.degrowth.net/uploads/upload_0e11629d4bd832b0b560795913af2817.png)

ospf协议部署成功后，我们可以使用ping命令检测处于不同网段网络的是否连通。对于rip协议的部署，与ospf部署过程相同，这里不再进行介绍。

在成功部署RIP/OSPF路由协议后，为了更加深入了解RIP/OSPF路由协议，接下来我们将使用tcpdump抓包工具抓包分析RIP/OSPF路由协议。

### 实验三 抓包分析RIP协议

#### 实验目标

在成功部署RIP路由协议的网络中使用tcpdump抓包工具抓包分析rip路由协议。

#### 实验原理

**rip路由协议介绍**

1、rip路由协议简介
路由信息协议RIP（Routing Information Protocol），是一种比较简单的内部网关协议（IGP）,它是一种基于距离矢量（Distance-Vector）算法的协议，使用了基于距离矢量的贝尔曼-福特算法（Bellman-Ford）来计算到达目的网络的最佳路径,使用跳数作为度量来衡量到达目的网络的距离。

2、rip协议工作原理
* 初始时，每个路由器上的路由表只记录与本身直连的网络。
* rip协议运行后，每台路由器都会定期与邻居路由交换路由表信息，路由器在收到新的路由表信息后会根据收到的路由表信息更新自身的路由表。其更新过程如下：
    

![](https://pad.degrowth.net/uploads/upload_bade0642988a8bce99b25977d600a40f.jpg)

3、rip协议数据包格式
4、rip协议缺点
rip协议存在的一个问题是当网络出现故障时，要经过比较长的时间才能将故障信息传送到所有的路由器。称为“好消息传的快，坏消息传的慢”。

#### 实验步骤

1、使用sudo tcpdump -i eth0 src 192.168.101.10 -w ./rip.pcap命令，将抓取到的数据包存入rip.pcap文件中，再导入到wireshark中查看详细信息。使用wireshark筛选出包含rip协议的数据包如下：

![](https://pad.degrowth.net/uploads/upload_8f73a7f29e8103e83dd3e67fcfe88910.png)

注意：当树莓派（路由器）上的ripd服务运行一段时间生成相应的路由表后，可能不会抓取到完成的数据包，因此建议先运行抓包命令后再在树莓派上启动ripd服务，这样rip路由协议在生成路由表过程中产生的数据包都会被抓取到。（在抓包分析ospf路由协议过程中也有类似的情况需要注意）

2、分析抓取到的不同类型的数据包（与rip路由协议原理对照分析）

### 实验四 抓包分析OSPF协议

#### 实验目标

在成功部署ospf路由协议的网络中使用tcpdump抓包工具抓包分析ospf路由协议。

#### 实验原理

**ospf路由协议介绍**

#### 实验步骤

1、使用sudo tcpdump -i eth0 src 192.168.101.10 -w ./ospf.pcap命令，将抓取到的数据包存入ospf.pcap文件中，再导入到wireshark中查看详细信息。使用wireshark筛选出包含ospf协议的数据包如下：

![](https://pad.degrowth.net/uploads/upload_b6f75d4558adeb880ad2926501c724d1.png)

2、分析抓取到的不同类型的数据包（与ospf路由协议原理对照分析）

通过这一章的实验，我们已经搭建好一个小型的局域网，并且处于这个局域网中的节点都已经连通起来。但是考虑到我们在这一章中搭建的网络需要专门的节点来做为路由器，当连接到路由器上的节点数量过多的话，路由器节点的负载将会大大增加，同时在这个网络中需要网线连接我们的路由器，这样我们的网络的覆盖范围就会大大受限。并且在我们的物联网系统中，我们希望系统中的节点是可以随时加入和退出的，这具有很强的动态型，因此本章搭建的网络不是很适合我们的物联网系统，那么有没有其它的网络架构符合我们的需求呢？答案是肯定的，mesh网络是物联网系统中使用广泛的一种网络架构。在下一章的实验中，我们将详细介绍一下mesh网络，并使用树莓派节点搭建一个mesh网络。

## 第三章 adhoc网络搭建与olsrd路由
   在第二章中，我们搭建了物联网中常用的mesh网络，mesh网络具有分布式与静态的特点，它需要由树莓派作为AP组成主干网后，其他节点才能连接进入网络。在室内环境下，我们可以轻松的搭建主干网，但是在野外我们没有条件搭建基础设施时该怎么办呢？同时我们还要考虑到野外情况下，我们的终端可能会随时移动或者产生损耗而失效。
   面对这种情况，我们就要使用一种更加灵活的网络架构，称为adhoc网络。与mesh网络相比，adhoc整个网络没有固定的基础设施，通过wifi进行连接，每个节点都是移动的，并且都能以任意方式动态地保持与其它节点的联系，自发的接入网络。
   
  
  ![](https://pad.degrowth.net/uploads/upload_431886d10cf63cfe6ae3c7b54c991999.png)

如上图所示，adhoc网络拓扑是一个强连通图，任意两个节点之间都有至少一条路径可以进行通信，且每一个节点在网络中的地位相同，也就是说任意增加/删除一个节点并不会对整个网络造成影响。在这一章中，我们将搭建adhoc来作为户外物联网系统的基础网络。Adhoc网络拥有自动添加节点的特性，同学们只要设置好参数，设备便可以自动加入网络中。废话不多说，我们开始搭建adhoc网络。

### 实验一 Adhoc网络组建
&lt;!-- **注：由于配置adhoc网络后不能连接互联网，故在配置前应先将软件和一些需要的包安装好，此处仅提供组网需要的包，其他软件如k8s、docker等应在此之前安装。**
	
所需的包：链接:[https://pan.baidu.com/s/1gCSOwyd7gFoMvCoM-k1F0Q](https://pan.baidu.com/s/1gCSOwyd7gFoMvCoM-k1F0Q)**密码: eakm
	
需要安装的包：

```shell=
Vim编辑器：sudo apt-get install vim
Mqtt对应Python（或其他语言）包：pip install paho-mqtt
```

注：mqtt broker所在服务器节点需要额外部署broker，即下载mqtt包并部署，具体方法为解压后，运行 ./emqx/bin.emqx start（本文档所需要的压缩文件均包含在文档开头

```shell=
安装bison语法分析器 sudo apt-get install bison
安装flex语法分析器 sudo apt-get install flex
安装抓包工具 sudo apt-get install tcpdump
``` --&gt;
&lt;!-- ### 实验一 组网实验 --&gt;

#### 实验目标：

使用树莓派进行组网实验，通过实验了解adhoc网络与mesh网络的区别，理解网络ip、网关等对网络的影响，搭建adhoc网络，并在网络中部署路由算法，使得网络中各个节点之间可以通信。

#### 实验工具：

树莓派4b、网卡

#### 实验内容要求

三人及以上以上为一组，通过设置树莓派网卡，进行组网实验，组网成功后，3个节点可以互相ping通，并使用tcpdump抓包。
为了组网，我们需要修改Linux的网络接口配置文件：sudo vim /etc/network/interface， 参考以下内容进行修改后添加至文件：

```plain
auto wlan0
iface wlan0 inet static
address 10.0.0.6
netmask 255.255.255.0
network 10.0.0.0
broadcast 10.0.0.255
wireless-channel 3
wireless-essid mypi
wireless-mode ad-hoc
```
其中
* adress为节点IP地址，每个节点的adress应不相同，此处仅为举例，同一网络的节点应在同一网段，应为1-255之间，
* wlan0为组网对应的网卡名称，可通过ifconfig命令查看。
* wireless-essid为adhoc网络名称，同一网络名称应相同
* wireless-channel为网络信道，应在3-7之间
* 修改后使用
```shell=
sudo /etc/init.d/networking restart
```
命令重启网络，或者重启设备即可。
	
使用ifconfig和iwconfig命令查看组网结果，若对应网卡显示出了正确的ip、mode、essid和cell则说明组网成功。
	
若使用此方法不成功则使用手动修改网卡的方式（此方式每次重启设备需要重新再部署一次）：

```shell=
sudo ifconfig wlan0 10.0.0.6  
sudo iwconfig wlan0 essid “mypi”
```

部署后若ping不通，可能是arp表中缺少表项，添加即可

```shell=
sudo arp -s 10.0.0.3（目的主机ip） xx:xx:xx:xx:xx:xx（目的主机的MAC地址）
```
数据在网络中进行传输时，依赖的是MAC地址而不是ip地址，因此需要使用arp协议将ip地址转化为mac地址，这里我们手动更新arp表，使得它们之间能够通信。
![](https://pad.degrowth.net/uploads/upload_0eac1fedf3eff7f910a0474bf435deb9.png)

adhoc网络中的节点之间通信时，如果不能直接与目的节点通信，就需要通过路由转发的方式来进行消息传递，为此，我们需要在网络中运行一个路由协议，建立路由表，让节点可以和网络中任意一个节点进行通信。在第二章的实验中，我们已经了解到了一些传统的路由协议，例如，RIP协议和OSPF协议，那么我们可以直接在把这些传统的路由协议直接用于adhoc网络吗？要解答这个问题，我们先回顾一下adhoc网络的特点。在adhoc网络中，所有节点的地位平等，是一个对等式网络，结点可以随时加入和离开网络，也可以随处移动，随时开机和关机。当结点要与其覆盖范围之外的结点进行通信时，可以通过中间结点进行转发，一个节点的故障不会影响到整个网络。我们看出adhoc网络拓扑的是动态变化的，如果在adhoc网络中使用传统的路由协议，可能会出现路由算法还未收敛，网络拓扑结构就发生了变化的问题，这将会在路由发现和维护上付出很大的代价。除此之外，adhoc网络的系统带宽、能量等资源是有限的，而传统的路由协议在设计时没有考虑资源限制的问题。因此，我们一般不在自组网络中使用传统的路由协议。

针对无线自组网的特点与需要，目前有许多路由协议被提了出来。感兴趣的同学可以查阅资料深入学习和了解。本次实验中，我们将使用OLSRD路由协议。下面我们将进行OLSRD路由协议部署的实验。

### 实验二 OLSRD协议部署

#### 实验目标

在实验一中组建好的adhoc网络中，部署olsr路由协议，使得网络中的每个节点能够相互连通。

#### 实验步骤

首先下载olsr路由协议包，下载链接:https://pan.baidu.com/s/1gCSOwyd7gFoMvCoM-k1F0Q**密码: eakm。下载解压后，进入解压后的olsr文件夹，使用make命令编译，再使用sudo make install命令安装即可。运行协议前，需要修改协议的配置文件sudo vim /etc/olsrd/olsrd.conf，注释或删除掉最下方两个语句以及括号仅留这两项即可，修改后的文件内容如下：

```shell
LinkQualityFishEye             0
Interface &#34;wlan0&#34;
{
}
#LoadPlugin &#34;olsrd_httpinfo.so.0.1&#34;
#{
#}
#
#LoadPlugin &#34;olsrd_jsoninfo.so.0.0&#34;
#{
#}
```

此外，应关闭网卡的ipv6协议，打开网卡的包转发功能，sudo vim /etc/sysctl.conf在最下方添加如下内容：

```
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
net.ipv6.conf.lo.disable_ipv6=1
net.ipv4.ip_forward=1
```

然后使用sudo sysctl -p使其生效，使用sudo olsrd运行协议。协议部署成功后，接下来我们就可以使用tcpdump抓包工具抓包分析olsr路由协议。

### 实验二 tcpdump抓包工具：
如果在上面的实验中可以ping通各个节点，那么恭喜同学们，网络已经组建好了，下面我们将使用Linux的抓包工具来对网络进行抓包分析；通常抓包时我们会使用下面两种工具：
* WireShark主要应用于windows，提供图形界面。
* tcpdump也应该掌握，同时还要能够学会与WireShark的配合，例如导出cap软件在WireShark中显示。在此基础上，让学生了解抓包原理，并且能够自行撰写数据包分析程序。
1、语法
tcpdump采用命令行方式，它的命令格式为：
```shell=
tcpdump [ -adeflnNOpqStvx ] [ -c 数量 ] [ -F 文件名 ]
　　　　　　　　　　[ -i 网络接口 ] [ -r 文件名] [ -s snaplen ]
　　　　　　　　　　[ -T 类型 ] [ -w 文件名 ] [表达式 ]
```

　　1. tcpdump的选项介绍

| 参数 | 作用 |
| :------: | :------: | 
|   -a   |   将网络地址和广播地址转变成名字；   | 
|   -d   |   将匹配信息包的代码以人们能够理解的汇编格式给出；   |　 　　　
|  -dd   | 　　　将匹配信息包的代码以c语言程序段的格式给出；|
| -ddd   | 　　　将匹配信息包的代码以十进制的形式给出；|
|　 -e   | 　　　在输出行打印出数据链路层的头部信息；|
|　 -f   | 　　　将外部的Internet地址以数字的形式打印出来；|
|　 -l   | 　　　使标准输出变为缓冲行形式；|
|　 -n   | 　　　不把网络地址转换成名字；|
| 　-t   | 　　　在输出的每一行不打印时间戳；|
|　 -v   | 　　　输出一个稍微详细的信息，例如在ip包中可以包括ttl和服务类型的信息；|
|　-vv   | 　　　输出详细的报文信息；|
|　 -c   | 　　　在收到指定的包的数目后，tcpdump就会停止；|
|　 -F   | 　　　从指定的文件中读取表达式,忽略其它的表达式；|
|　 -i   | 　　　指定监听的网络接口；|
|　 -r   | 　　　从指定的文件中读取包(这些包一般通过-w选项产生)；|
|　 -w   | 　　　直接将包写入文件中，并不分析和打印出来；|
|　 -T   | 　　　将监听到的包直接解释为指定的类型的报文，常见的类型有rpc （远程过程调用）和snmp（简单网络管理协议；）|

2、关键字

**缺省类型的关键字：**
| 关键字 | 作用 |
| :----: | :----: |
| host(缺省类型):    | 指明一台主机，如：host 210.27.48.2   |
| net:    | 指明一个网络地址，如：net 202.0.0.0  |
| port:    | 指明端口号，如：port 23  |


**确定方向的关键字：**


| 关键字 | 作用 |
| :----: |:----: |
| src:    | src 210.27.48.2, IP包源地址是210.27.48.2    |
| dst:    | dst net 202.0.0.0, 目标网络地址是202.0.0.0   |
| dst or src(缺省值)    | 源地址或目的地址 |
| dst and src(缺省值)    | 源地址和目的地址 |


**协议的关键字：**

缺省值是监听所有协议的信息包

fddi、ip、arp、rarp、tcp、udp等

**其他关键字**：

gateway

broadcast

less

greater

**常用表达式：多条件时可以用括号，但是要用转义**

非 : ! or “not” (去掉双引号)

且 : &amp;&amp; or “and”

或 : || or “or”

**1.1 tcpdump -D查看哪些端口可进行抓包**
![](https://pad.degrowth.net/uploads/upload_5ed0173bb9643bddca4dbe759cc45421.png)

**1.2 tcpdump -i eth0 -c 10进行抓包**
![](https://pad.degrowth.net/uploads/upload_31e73933bbcb8f29b8a84fee16c21797.png)

**1.3 包保存到指定文件-w，从指定文件读取包答应到屏幕-r**
![](https://pad.degrowth.net/uploads/upload_b462347b3f52ba78024a1fbd99febbda.png)

**1.4 过滤指定主机**
（附截图）
**1.5 源与目的，src与dst**
```shell=
tcpdump -i eth0 -c 10 -nn src host xxx tcpdump -i eth0 -c 10 -nn dst host xxx
```
![](https://pad.degrowth.net/uploads/upload_6f2a429f8cf1111bdb7b95fd0b92aae8.png)

**1.6 协议过滤(UDP、TCP、ICMP)**
过滤udp协议，可能服务器上目前没有udp的包，再开一个窗口，自己创造点儿包出来，用
```shell=
curl  http://www.baidu.com；curl http://www.hao123.com
curl http://www.163.com 
tcpdump -i eth0 -c 10 -nn udp
```
![](https://pad.degrowth.net/uploads/upload_9ea926ec25e033bee517d89e52790db8.png)

**1.7 过滤网段**
```shell=
tcpdump -i eth0 -c 10 -nn net 192.168
```
![](https://pad.degrowth.net/uploads/upload_949aa62787112f4b3c1070677a05cc61.png)

**1.8 协议字段过滤**
表达式单元之间可以使用操作符&#34; and / &amp;&amp; / or / || / not / ! &#34;进行连接
过滤syn包和fin包
```shell=
tcpdump -i eth0 -c 10 -nn &#39;tcp[tcpflags] &amp; (tcp-syn|tcp-fin) != 0&#39;
```
![](https://pad.degrowth.net/uploads/upload_edc3e9e5dcb26e72123a200b829cd478.png)

通过实验二，我们学会使用tcpdump抓包工具进行抓包，接下来我们继续进行我们的实验。在本章的实验一中，我们是在路由器上制定了默认网关，使得处在不同网段的网络能够相互通信，在下面的实验中，我们将不在路由器上指定网关，而是在上面部署相关的路由协议，根据路由协议来进行路由，使得网络中的各个节点能够相互连通。接下来就来介绍如何在路由器上部署RIP/OSPF路由协议。


### 实验三 tcpdump抓包分析OLSR协议

#### 实验目标

在部署olsr协议的adhoc网络中使用tcpdump抓包分析oslr路由协议。

#### 实验原理

**OLSR路由协议介绍**

#### 实验步骤

本次实验只抓取了两个节点的数据包，通过对抓取到的数据包分析对olsr路由协议进行初步的分析。实验使用到两个节点的ip分别为10.0.0.3和10.0.0.102，首先使用route命令查看这两个节点的路由表。
10.0.0.0.3节点路由表:
![img](https://pad.degrowth.net/uploads/upload_e52ec97b10960bcb8d61b5e099e6f845.png)
10.0.0.102节点路由表:
![img](https://pad.degrowth.net/uploads/upload_1fab7e9cdbed50b3565404693fcef542.png)
路由表中的Flags字段H表明目标是一个主机，字段为G表明为默认路由，当目标主机不能在路由表中查找到时，数据包就被发送到默认路由上。上图红线框中的部分表示10.0.0.3和10.0.0.102节点是直接连通的，可以直接传输数据。

下面使用tcpdump进行抓包，使用如下命令抓取两个节点之间的数据包：

```
sudo tcpdump -i wlan0 -c 10 host 10.0.0.3 or 10.0.0.102 -w .\result.pcap
```

![img](https://pad.degrowth.net/uploads/upload_aa26740e00b21a4e630cfbb593a2abdb.png)
将生成的result.pcap文件导入到wireshark中进行查看,结果如下图所示:
![img](https://pad.degrowth.net/uploads/upload_b0674e6c0b63fd9d9a20e0eeb0b99213.png)
OLSR协议较为关键的消息类型为HELLO消息和TC消息，下面对抓取到的HELLO消息和TC消息进行介绍:
HELLO消息格式如下：
![img](https://pad.degrowth.net/uploads/upload_74f536e6e15e12c29c55315a75d1c576.png)
OLSR通过周期性地广播HELLO消息来探测邻居节点，建立邻居表。HELLO消息只在一跳范围内传输。
HELLO消息各个字段的含义如下:

- Reserved是保留字段，为“0000000000000000”。
- Htime是发送HELLO消息的时间间隔。
- Willingness表示该节点是否愿意为其他节点转发消息，分别为WILL_NEVER(该类型节点不会被选做MPR节点),WILL_ALWAYS(会被选做为MPR节点)以及默认的WILL_DEFAULT。
- Link Code描述了当前节点与下面要发送的邻居节点的连接状态。如果Link Code的值小于等于15，它被解释为保存了两个不同的字段。每个字段长2bits：
  ![img](https://pad.degrowth.net/uploads/upload_5d8ec3890ee6992c5d741d9a7a404623.png)
  Link Types有以下四种类型:UNSPEC_LINK,ASYM_LINK,SYS_LINK,LOST_LINK
  Neighbor Types有三种类型：SYS-NEIGH,MPR_NEIGH,NOT_NEIGH.
- Link Message Size表示从当前Link Code到下一个Link Code的长度,若无下一个Link Code则到分组结尾。
- Neighbor Interface Address为邻居节点的IP地址。
  抓取到的HELLO消息如下：
  ![img](https://pad.degrowth.net/uploads/upload_9034d5ffd9f00e7965ee949fde804b2d.png)
  TC消息的格式如下：
  ![img](https://pad.degrowth.net/uploads/upload_fd0491363499da36135a395f76c9caa1.png)
  TC消息为拓扑控制消息，每个节点都会周期性的发送TC消息，当一个节点收到TC消息时，就会进入拓扑信息维护模块来更新拓扑表。
  各个字段的含义如下：
- ANSN(Advertised Neighbor Sequence Number),当该节点的MPR节点集发生变化时，该字段的值会增加。接受到TC分组信息的节点可以根据该字段值的大小来判断收到的信息是否为最新的。(Advertised Neighbor Node为MPR节点)
- Advertised Neighbor Main Address是选择这个节点作为MPR的节点的集合，也称为MPR Selector
  抓取到的TC消息如下：
  ![](https://pad.degrowth.net/uploads/upload_a8139b25fd5782bd0265579a22d1eb65.png)

&lt;!-- 对于一个网络拓扑，通常我们还想要监测网络拓扑的状态和各个节点的连接情况，对于上述需求，我们可以使用OpenWrt开发板来帮助我们实现。下面我们就来使用OpenWrt开发板来查看网络拓扑。 --&gt;
&lt;!-- ### 实验四 OpenWrt实验

#### 实验目标

OpenWrt是一个针对嵌入式设备的Linux操作系统。提供了一个完全可写的文件系统和包管理。允许用户通过使用包来定制设备以适应任何应用程序。本次实验将实现以下三部分的内容：
（1）更新OpenWrt开发板固件。
（2）将OpenWrt开发板节点加入到之前组建好的adhoc网络中。
（3）在OpenWrt开发板节点上配置olsrd路由协议。

#### 实验步骤

1、OpenWrt开发环境搭建
2、OpenWrt开发板配置olsr路由协议
3、OpenWrt节点加入adhoc网络中

通过上面的实验，我们已经搭建好一个adhoc网络，并部署了olsr路由协议，现在，网络中的任意两个节点都可进行通信，在下面一个实验中，我们将介绍如何在搭建好的网络中传输节点收集到的传感器数据。
 --&gt;
### 实验四 socket通信实验
#### 实验目标
在前面两个实验中我们搭建好了adhoc网络并且学会了如何抓包，所以接下来我们尝试在adhoc网络中进行传感器数据的传输。那么我们首先采用比较常见且相对简单的socket进行通信，我们简单了解一下socket，socket又称套接字，是在应用层和传输层之间的一个抽象层，socket本质是编程接口(API)，它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。TCP/IP只是一个协议栈，必须要具体实现，同时还要提供对外的操作接口（API），这就是Socket接口。通过Socket,我们才能使用TCP/IP协议。当两个主机的程序之间要通过网络进行套接字通信的时候，我们可以将这过程抽象成下面这张简图。
![](https://pad.degrowth.net/uploads/upload_f8296299cb067e4188251cb757e4032d.png)
#### 实验步骤
##### 步骤一、编写socket通信客户端程序
在正式传输传感器之前，我们还需要做一个额外的工作，虽然在实验一中我们已经完成了向树莓派传输传感器数据，但此时传输的数据是众多传感器数据的集合，并不是我们想要的真正数据，因此我们需要将各个传感器分别剖析出来，所以首先我们需要对从传感器中获得的数据进行预处理，去除掉无用的换行符，随后对数据进行拆分，提取出对应的传感器名称和数值。另外由于传感器输出的信号都是包括电压、电流、应变、温度等人可以直观认知的模拟信号，但是在STM32这种单片机上只能识别数字信号，所以单片机上一般都配有ADC(ADC，Analog-to-Digital Converter模数转换器)，因此我们在树莓派上接收的数据除了温度传感器数据是准确的以外，其余传感器对应的测量值都是传感器读取的ADC值，所以在这里我们需要将其余传感器的数值进行相应转换，以便得到真正的数据。
```python=
def dealData(data):
    data = data.strip(&#39;\n&#39;)
    dataSplitResult = data.split(&#39;:&#39;)
    if len(dataSplitResult) == 2:
        sensorName = dataSplitResult[0]
        ADCValue = float(dataSplitResult[1])
    else:
        return 0
    if sensorName == &#39;Temperature&#39;:
        return sensorName + &#34;:&#34; + str(ADCValue)
    else:
        sensorValue = getSensorValue(ADCValue)
        return sensorName + &#34;:&#34; + str(sensorValue)
def getSensorValue(ADCValue):
    if (ADCValue &gt; 4000):
        ADCValue = 4000
    Value = 100 - (ADCValue / 40)
    return format(Value, &#39;.2f&#39;)
```
经过数据的重新处理之后，我们就可以通过socket进行数据的传输了。首先在任何类型的通信开始之前，网络应用程序都必须创建套接字。我们知道同一主机的两个进程如果需要进行通讯最基本的一个前提是能够唯一的标示一个进程，在本地进程通讯中我们可以使用PID（Process Identification，操作系统里指进程识别号，也就是进程标识符。操作系统里每打开一个程序都会创建一个进程ID，即PID）来唯一标示一个进程，但PID只在本地唯一，网络中的两个进程PID冲突几率很大，这时候我们需要另辟它径了。我们知道IP层的ip地址可以唯一标示主机，而TCP层协议和端口号可以唯一标示主机的一个进程，这样我们可以利用ip地址＋协议＋端口号唯一标示网络中的一个进程。因为UDP面向无链接。所以基于UDP的socket通信地实现少了连接与接收连接的操作，大大减少了资源的消耗，所以在这里我们初步使用基于UDP的socket通信，于是下面的代码应运而生，IP地址（10.0.0.3），协议（UDP），端口号（9998），这样我们就可唯一标识网络中的一个进程，实现不同主机之间相互通信，我们使用 socket 模块的 socket 函数来创建一个 socket 对象。socket 对象可以通过调用其他函数来设置一个 socket 服务，实现数据发送的操作。
```python=
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
host=&#34;10.0.0.3&#34;
port=9998
s.sendto(str(data).encode(&#39;utf-8&#39;),(host,port))
```
在发送传感器数据时我们只需要将data换为预处理之后的数据即可，发送端实验效果如下图
![](https://pad.degrowth.net/uploads/upload_a0a1201e31f757c9cf82c3b08ea6fff7.png)



##### 实验步骤二、编写通信服务器程序
在实现客户端发送数据的功能后，我们需要在服务器端完成数据的接收打印，socket通信的过程是客户端的信息直接发送到服务端套接字，因此在服务器端我们只需要对信息进行接收打印即可。服务器首先建立通信连接，建立过程同客服端一样。
```python=
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((host,port))
```
同客服端的连接建立成功之后就可以进行信息的接收了
```python=
receive_data,addr = s.recvfrom(1024)
```
上诉代码解释为：一次性接受1024bytes的数据 ，返回一个元组，其中receive_data是接收的数据、addr是发送端的IP地址。服务器端实验效果如下图:
![](https://pad.degrowth.net/uploads/upload_2cdc8cf1d0deb60ca457e0eee8f212d3.png)

在完成了socket通信之后，我们利用前面使用的Linux抓包工具tcpdump来对网络进行抓包分析，可以看到socket通信过程中传输的信息能够被准确的抓包分析出。
![](https://pad.degrowth.net/uploads/upload_23fdded55b49588ee3b07b887bcec557.png)



## 第四章 报文AES对称加密
#### 实验目标
通过前在面的socket实验抓包中我们可以看到，在通过socket进行通信时，消息传输是通过明文传输的，我们通过使用tcpdump等工具是可以直接抓包明文信息，这样是极具安全隐患的。因此，我们需要对明文信息进行加密操作。采用何种加密算法是值得我们去研究的，加密算法分对称加密和非对称加密，其中对称加密算法的加密与解密密钥相同，非对称加密算法的加密密钥与解密密钥不同。因此，我们调研了各种不同的加密算法，譬如非对称加密算法RSA和ECC算法，其中RSA公开密钥密码体制的原理是：根据数论，寻求两个大素数比较简单，而将它们的乘积进行因式分解却极其困难，因此可以将乘积公开作为加密密钥；ECC算法中文名为椭圆加密算法，其数学基础是利用椭圆曲线上的有理点构成Abel加法群上椭圆离散对数的计算困难性。同时也调研了对称加密算法DES和AES算法等等，DES算法为密码体制中的对称密码体制，又被称为美国数据加密标准，AES的出现也是为了取代DES算法。考虑到我们的应用场景为树莓派结点，其算力资源十分有限，非对称加密算法对资源消耗都比较高，因此我们暂时未做考虑，在对称加密算法中，AES与其他加密算法相比，AES加密算法运算速度快，安全性高且资源消耗低，这一优点和我们边缘设备十分契合。因此我们在实验中将采用AES算法进行消息的加解密。
#### 实验步骤
##### 1、编写AES对称加密算法
在正式进行编写之前，我们先介绍一下AES加密算法，AES属于分组加密算法，在加密时，AES将明文划分为等长128位(16字节)的明文块，对于一段很长的明文，需要不断迭代出固定的长度进行加密；对于最后不够固定长度的明文需要补齐至固定长度，最终全部加密。它可以使用128、192和256位字符作为密钥，下面介绍一下AES算法具体的加密流程。
![](https://pad.degrowth.net/uploads/upload_ecde499837a96cf61ff0adcefcaacd0d.png)

最开始算法将输入的明文分成固定的块，并将数据按照下图进行排列。
![](https://pad.degrowth.net/uploads/upload_37507f682e8d2cf73c33de9f12c5a298.png)
随后，AES加密过程会多次重复以下4个步骤：分别是SubBytes(字节替代)、(ShiftRows)行移位、(MixColumns)列混淆和(AddRoundkey)轮密钥加。解密过程分别为对应的逆操作。由于每一步操作都是可逆的，按照相反的顺序进行解密即可恢复明文，加解密中每轮的密钥分别由初始密钥扩展得到。
* SubBytes：将每一组16字节的明文数据以每个字节的值(0-255)为索引，从一张拥有256个值得表中查找对应值进行替换(类似Base64的查表替换)。
* ShiftRows：将以4字节为单位的行按照一定的规则向左平移，且每一行平移的字节数不同。
* MixColumns：对一个4字节的值进行位运算，将其变为另一个4字节的值。
* AddRoundkey：将MixColumns的输出与轮密钥进行xor异或。
![](https://pad.degrowth.net/uploads/upload_baf6dcc2cd6fd6210677f6ca1bf55d4e.png)

*注：128、192和256位字符分别需要10、12和14轮运算，本节以128位秘钥作为示例，其中最后一轮运算将不会进行列混合计算。*

---
在介绍完AES加密算法的基本原理之后，我们开始用python编写算法。
环境安装

```shell=
pip uninstall crypto
pip uninstall pycryptodome
pip install pycryptodome
```
在前面我们说到AES算法需要将输入的明文进行分组，AES常用的分组模式有就是Electronic CodeBook mode（电子密码本模式即ECB模式 和Cipher Block Chaining mode（密码分组链接模式即CBC模式，当然还有很多其它模式，他们都属于AES加密。ECB模式和CBC模式俩者区别就是 ECB不需要iv偏移量，而CBC需要。前面说到当最后一个明文分组的内容小于分组长度时，需要用一些特定的数据进行填充，AES支持的常用填充方式有Zeros和PKCS7，Zeros即填充字符串由设置为零的字节组成，PKCS7即填充字节序列，缺几个字节就填几个缺的字节数。
PKCS7填充示例
```shell=
... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
```
由此生成以下代码
```python=
 def __ZeroPadding(self,data):
        data += b&#39;\x00&#39;
        while len(data) % 16 != 0:
            data += b&#39;\x00&#39;
        return data
    
 def __PKCS5_7Padding(self,data):
        needSize = 16-len(data) % 16
        if needSize == 0:
            needSize = 16
        return data + needSize.to_bytes(1,&#39;little&#39;)*needSize
```
基于python的AES加密具体过程在AES库中已经集成，因此我们利用AES库，在了解AES加密原理的前提下，完成数据的预处理，最终实现加密过程。
##### 2、实现socket的AES加密通信，将传感器数据实现加密传输
步骤一中我们介绍并完成AES加密算法，因此接下来我们在客户端(10.0.0.4)进行传感器数据的加密操作，然后将加密后的数据进行传输，但是在我们客户端本身，我们为了能够观察到具体的传感器情况，因此我们在界面上输出原始的数据。

![](https://pad.degrowth.net/uploads/upload_1c35b2fac751a1392ff9e6282db9103a.png)
---
在收到客户端发来的加密信息之后，服务器结点(10.0.0.3)对收到的数据进行解密操作，服务器得到易读的数据。
![](https://pad.degrowth.net/uploads/upload_08d061b017b4dcd0a16782edf1319491.png)

经过对传输数据的加解密，我们在算法层面初步保证了数据的安全性，但实际传输过程中我们是否传输的是密文则需要对传输的信息进行抓包，通过下面的网络信息抓包可以发现在传输过程中的数据是经过算法加密后数据，以此保证了信息的安全性。
![](https://pad.degrowth.net/uploads/upload_6e044d75010c47c9c219eec18fec990b.png)


## 第五章 MQTT协议
#### 实验目标
在前面一章的实验中，我们实现了利用socket进行传感器数据的加密传输，但无论是基于TCP或者是UDP的socket通信都有这不小的弊端，基于TCP的socket通信在建立连接时需要较多的时间，增加了较多的系统资源开销，而基于UDP的socket通信是无连接的，虽然减少了连接的资源开销，但是只要知道接收端的IP和端口，且网络是可达的，任何主机都可以向接收端发送数据。这时候，如果一次能读取超过一个报文的数据，则会乱套。比如，主机A向发送了报文P1，主机B发送了报文P2，如果能够读取超过一个报文的数据，那么就会将P1和P2的数据合并在了一起，这样的数据是没有意义的。因此我们需要选择一个更好的传输协议，基于我们的物联网架构，MQTT协议毫无疑问成为首选。接下来我们将介绍MQTT协议以及MQTT协议为何如此适合我们的实验环境。MQTT全称为 Message Queuing Telemetry Transport（消息队列遥测传输）是一种基于发布/订阅范式的“轻量级”消息协议，由 IBM 发布，该协议支持所有平台，几乎可以把所有联网物品和外部连接起来，被用来当做传感器和致动器（比如通过Twitter让房屋联网）的通信协议。下图为MQTT协议的基础架构图。

![](https://pad.degrowth.net/uploads/upload_a06e6889ba2eaa3c6f62e5d85b4cf411.png)

MQTT协议可以被解释为一种低开销，低带宽占用的即时通讯协议，它适用于硬件性能低下的远程设备以及网络状况糟糕的环境下，因此 MQTT 协议在 IoT（Internet of things，物联网），小型设备应用，移动应用等方面有较广泛的应用。下图为一个简单的MQTT的应用场景。MQTT使用的发布/订阅消息模式，它提供了一对多的消息分发机制，从而实现与应用程序的解耦，这是一种消息传递模式，消息不是直接从发送器发送到接收器（即点对点），而是由MQTT server（或称为 MQTT Broker）分发的。MQTT遵循精简，不添加可有可无的功能、把传输量降到最低以提高传输效率、把低带宽、高延迟、不稳定的网络等因素考虑在内、假设数据不可知，不强求传输数据的类型与格式，保持灵活性等设计原则，由于物联网的环境是非常特别的，因此MQTT是物联网数据传输的首选协议，它用极少的代码和有限的带宽，为连接远程设备提供实时可靠的消息服务。以上特点让 MQTT 协议非常适合计算能力有限、网络带宽低、信号不稳定的远程设备，因此，MQTT协议比Socket更加适合我们的实验。

![](https://pad.degrowth.net/uploads/upload_a13dea8bbee3a1c61a48113d8b04c989.png)

本实验旨在让学生了解并学会使用MQTT协议。搭建本地MQTT服务器并通过MQTT协议传输传感器数据。
#### 实验步骤
##### 1、搭建mqtt服务器
参考命令
* 下载
```shell=
wget https://www.emqx.com/zh/downloads/broker/4.4.1/emqx-4.4.1-otp24.1.5-3-ubuntu20.04-arm64.zip
```
* 安装
```shell=
unzip emqx-4.4.1-otp24.1.5-3-ubuntu20.04-arm64.zip
```
* 运行
```shell=
./bin/emqx start
```
* 环境配置
```shell=
pip3 install -i https://pypi.doubanio.com/simple paho-mqtt
```
##### 2、实现传感器数据mqtt传输
从前面的MQTT介绍中我们可以知道，MQTT协议使用的是发布/订阅消息模式，所以我们在客服端建立与MQTT服务器的连接之后，将话题(tocpic)与我们的实际信息进行绑定，这样发送的数据也便有了属于自己的身份，其余客服端也能通过订阅话题从而收到数据。
首先导入paho.mqtt客服端
```python=
from paho.mqtt import client as mqtt
```
然后建立起客服端1即发送端与MQTT服务器的连接，定义连接成功回调函数on_connect，从而判断连接是否成功。
```python=
def on_connect(client, userdata, flags, rc):
    print(&#34;Connected with result code: &#34; + str(rc))
if __name__ == &#39;__main__&#39;:
    client = mqtt.Client()
    client.on_connect = on_connect
    client.connect(&#39;10.0.0.4&#39;, 1883, 600)
```
在连接建立成功后，我们就可以进行数据的发送了，我们发布主题为Iot_test的数据，数据在经过发送后不会直接到另外一个客户端而是会先到MQTT服务器，有需要该数据的客户端只需要订阅该主题执行接收即可。
发布topic
```python=
    client.publish(&#39;Iot_test&#39;, msg)
```
在初步学习了解完MQTT发送端程序编写之后，我们就可以进行传感器数据的发送了，同样像socket发送数据一样，我们只需要将客服端发送数据的msg替换成传感器数据就可以实现传感器数据的发送了。下面是客服端1也就是发送端界面。
![](https://pad.degrowth.net/uploads/upload_10984f6cdc3f21c7616a9976d8f5ccb0.png)

在发送端将传感器数据发送到MQTT服务器之后，客服端2也就是接收端就可以执行传感器数据的接收了，同发送端一样，首先我们需要建立与MQTT服务器的连接，定义连接成功过回调函数判断连接是否成功。这一点与发送端是无异的，与发送端不同的是在接受端我们需要定义消息接收回调函数，该函数可以返回订阅到的信息并指定想要的输出格式。
在定义连接成功回调函数后需要再定义消息回调函数，并在主函数需要指定消息回调函数。
```python=
def on_message(client, userdata, msg):
    print(f&#34;Received `{msg.payload.decode()}` from `{msg.topic}` topic&#34;)
```

```python=
if __name__ == &#39;__main__&#39;:
    client.on_message = on_message
```
最后，在接收端订阅物联网数据topic,并持续订阅和连接
```python=
client.subscribe(&#39;Iot_test&#39;, qos=0)
client.loop_forever()
```
下面是客服端2即接收端界面
![](https://pad.degrowth.net/uploads/upload_1a13a201587d3e280cf3208994cdd4d5.png)


在完成了MQTT通信传输传感器数据之后，我们利用前面使用的Linux抓包工具tcpdump来对网络进行抓包分析，可以看到MQTT通信过程中传输的信息也能够像socket通信一样被准确的抓包分析出，下图为抓包结果图：
![](https://pad.degrowth.net/uploads/upload_c9a7a0aca0262716c1c6fff4dfecf706.png)


## 第六章 MQTT报文AES对称加密
#### 实验目标
   在前面我们用MQTT实现了传感器数据的传输，但如同前面用socket传输数据一样，在最基础的mqtt通信中，信息的传输是明文的，而直接采用明文传输无法保证信息的安全，因此在本章中我们将糅合前面的知识包括使用串口读取传感器数据、AES加密算法和MQTT协议传输从而实现传感器数据的加密MQTT传输。本章的基础操作我们在socket加密通信实验中几乎都已经完成，只不过在之前我们使用的是socket通信，在这里换成了MQTT通信，因此我相信在认真做完了前面的实验之后这一章的代码对你们来说一定轻而易举！
   
我们在客户端(10.0.0.4)进行传感器数据的加密操作，然后将加密后的数据通过MQTT协议进行传输，但是在我们客户端本身，我们为了能够观察到具体的传感器情况，因此我们在界面上输出原始的数据
![](https://pad.degrowth.net/uploads/upload_795bf7a82dec2b72a45dac1fc25e1afb.png)


MQTT接收端解密，在收到客户端发来的加密信息之后，服务器结点(10.0.0.3)对收到的数据进行解密操作，服务器得到易读的数据。
![](https://pad.degrowth.net/uploads/upload_692a0cefe2760efbb5ea53ca33fe32f9.png)


经过对传输数据的加解密，我们在算法层面初步保证了数据的安全性，但实际传输过程中我们是否传输的是密文则需要对传输的信息进行抓包，通过下面的网络信息抓包可以发现在传输过程中的数据是经过算法加密后数据，以此保证了信息的安全性。
![](https://pad.degrowth.net/uploads/upload_6943dbb8e7853ce053f0446b1ca0c1c3.png)


## 第七章 时序数据库与数据可视化
### （待补充）什么是时序数据库？列举代表性产品，如influxDB数据库。

### （待补充）什么是数据可视化？列举代表性产品，如Granfana。

  在上一章中，我们将获得的传感器数据通过MQTT协议进行了传输，但传感器数据是实时更新的，一旦我们需要对这些数据进行分析时，数据的追溯就显得尤为重要，因此，我们现在急需一个数据库用来作数据存储。传感器数据拥有数量大，实时更新且与时间挂钩的特点，因此，在分析了众多数据库的种类后，发现此刻时序数据库显得十分地贴和我们的需求。InfluxDB是一个由InfluxData开发的开源时序型数据。它由Go写成，着力于高性能地查询与存储时序型数据，InfluxDB自带的各种特殊函数如求标准差，随机取样数据，统计数据变化比等，使数据统计和实时分析变得十分方便，适合用于包括DevOps监控，应用程序指标，物联网传感器数据和实时分析的后端存储。在经过互相比对发现，InfluxDB相较于其他时序数据库拥有操作简单，使用理念与MySql十分相似，大大方便了我们的操作，因此，我们最终选择采用InfluxDb数据库作为我们的后端数据库，influxDB数据库具有许多版本，influxDB:1.8版本是使用最广泛且比较稳定的版本，因此本次实验我们使用该版本，且需要注意的是因为CPU的架构不兼容，influxDB不能安装到树莓派原装系统，所以需要将数据库安装到ubuntu等系统。
 
### 实验一 influxDB数据库安装部署

#### 实验目标
本实验旨在linux系统上安装部署infulxDB数据库。并学会分别用shell命令形式和程序形式操作influxDB数据库。
##### 实验步骤1
安装部署influxDB数据库
参考部署命令
```shell=
wget https://dl.influxdata.com/influxdb/releases/influxdb_1.8.10_amd64.deb
sudo dpkg -i influxdb_1.8.10_amd64.deb
```
启动influxDB
```shell=
systemctl start influxdb #启动influxdb
systemctl status influxdb #查看状态
systemctl enable influxdb #开启开机启动
```
去往安装目录下，此时可以在shell中键入influx关键词进入InfluxDB的命令行
##### 实验步骤2
shell形式操作influxDB数据库。

######  influxDB基本操作命令
就像前面数据库介绍的那样，influxDB数据库和我们常用的MySQL数据库使用方法十分类似，在进入influxDB数据库后可以使用类sql语句进行数据库相关操作，包括创建、删除数据库和表；增加数据记录等等。下面我们介绍一下influxDB数据库的基础操作。
数据库与表的操作
```shell
#创建数据库
create database &#34;db_name&#34;
 
#显示所有的数据库
show databases
 
#删除数据库
drop database &#34;db_name&#34;
 
#使用数据库
use db_name
 
#显示该数据库中所有的表
show measurements
 
#创建表，直接在插入数据的时候指定表名
insert test,host=127.0.0.1,monitor_name=test count=1
 
#删除表
drop measurement &#34;measurement_name&#34;
```
增
```shell
&gt; use metrics
Using database metrics
&gt; insert test,host=127.0.0.1,monitor_name=test count=1
```
删
```shell
&gt; use metrics
Using database metrics
&gt; select * from test order by time desc
```
注：influxdb没有删除和修改操作，且具有相同tag的数据会被覆盖

##### 实验步骤3
##### 程序形式操作influxDB数据库
在实验步骤2中我们使用传统的shell命令进行influxDB数据库的操作，但在更多的场景中譬如物联网环境中，由于数据量较大且实时更新，采用人工手动操作显示是不实际的，因此，以程序操纵数据库进行数据记录写入等相关操作更加高效且便利。
同操作一般服务器，操作数据库首先需要建立连接，在连接建立过程中，我们需要指明ip地址、端口号、用户名、密码和将要操作的数据库。当数据库不存在时需要创建所需数据库,数据库存在时创建数据库语句并不会覆盖原始存在的数据。
```python=
from influxdb import InfluxDBClient
client = InfluxDBClient(&#39;ip地址&#39;, 8086(端口), &#39;root&#39;, &#39;root&#39;, &#39;example&#39;)
client.create_database(&#39;example&#39;)
```
在数据存入数据库时，数据都是以键值对的形式存入的，因此我们需要一种数据格式来进行数据交换，通过调研，现在主流的且现阶段最优的数据格式毫无疑问是JSON，JSON全称JavaScript Object Notation，是用于将结构化数据表示为JavaScript(一种具有函数优先的轻量级，解释型或即时编译型的编程语言)对象的标准格式，JSON数据格式比较简单, 易于读写,采用完全独立于编程语言的文本格式来存储和表示数据且JSON格式能够直接为服务器端代码使用，因此，我们可以将传感器数据以变量的形式写入JSON，在收集到一组完整的传感器数据之后我们将JSON数据写入influxDB数据库，从而实现传感器数据保存。基础JSON格式被指定为如下形式。
```json=
 json_body = [
    {
        &#34;measurement&#34;: &#34;iotdata&#34;,#指定表项
        &#34;tags&#34;: {
            &#34;host&#34;: &#34;server01&#34;,
            &#34;region&#34;: &#34;us-west&#34;
            &#34;tag&#34;:&#34;标志&#34;
        },
        &#34;time&#34;: time,
        &#34;fields&#34;: {
           &#34;value&#34;:12
        }
    }
```
JSON数据格式设计完成之后，我们就可以往influxDB数据库里面写入数据了。
```python=
client.write_points(json_body)
```
### 实验二 传感器数据保存
#### 实验目标
在实验一中我们完成了基本数据的写入，接下来我们实现传感器数据的写入首先定义JSON数据格式，本次实验收集到的传感器数据包括实时温度、最高温度、最低温度、当前阶段已知温度的平均温度；气体浓度、平均气体浓度；光照强度、平均光照强度；火焰值、平均火焰值。因此依据测试值JSON数据表可以设计为以下格式。

```json=
 json_body = [
    {
        &#34;measurement&#34;: &#34;iotdata&#34;,#指定表项
        &#34;tags&#34;: {
            &#34;host&#34;: &#34;server01&#34;,
            &#34;region&#34;: &#34;us-west&#34;
            &#34;tag&#34;:time
        },
        &#34;time&#34;: time,
        &#34;fields&#34;: {
            &#34;Temperature&#34;: Temperature ,
            &#34;maxTemperature&#34;:maxTemperature,
            &#34;minTemperature&#34;:minTemperature,
            &#34;averageTemperature&#34;:averageTemperature,
            &#34;Gas&#34;:Gas,
            &#34;averageGas&#34;:averageGas,
            &#34;Light&#34;:Light,
            &#34;averageLight&#34;:averageLight,
            &#34;Flame&#34;:Flame,
            &#34;averageFlame&#34;:averageFlame

        }
    }
]
```
我们只需要在MQTT接受端对接收到的数据进行数据处理，将数据一一对应赋值给JSON，然后执行写入操作就可以成功将传感器数据写入influxDB数据库。
#### 实验效果图
我们使用学会的shell命令对写入的数据库执行查询操作，发现可以在influxDB数据库中找到我们的传感器数据记录，由此，数据库操作成功完成。
![](https://pad.degrowth.net/uploads/upload_9340f091ca4405af9b5d1fb4fb3ffc51.png)
#### 实验参考代码
* 本章代码由前面的实验代码升级组合而来，我相信在认真做完了前面的实验之后这一章的代码对你们来说一定轻而易举！
### 实验三 数据可视化工具Granfana安装部署
在获得了传感器数据后，我们便要将这些数据展示出来。在这里我们将用到Grafana。
Grafana 是一个跨平台的开源的度量分析和可视化工具，可以通过将采集的数据查询然后可视化的展示，并及时通知。可以帮助我们对系统进行实时监控并将其可视化展示，使我们可以随时掌控系统的运行情况。Grafana支持的数据源有Graphite, InfluxDB, OpenTSDB, Prometheus, Elasticsearch, CloudWatch，Zabbix，同样支持OpenTSDB，InfluxDB等时序库。
我们要使用Grafana监测系统状态，并将传感器数据以图表形式展示出来。
#### 实验目标
   使用Grafana对传感器的数据进行分析，制作一些表格，将数据可视化的展示出来。本实验旨在linux操作系统上安装配置Grafana。
#### 实验内容
在linux操作系统上完成grafana安装配置。
#### 实验步骤
##### 下载安装apt-transport-https软件包
```shell=
sudo apt-get install -y apt-transport-https
```
##### apt-key是管理Linux系统中软件包密钥
```shell=
sudo apt-get install -y software-properties-common wget
```
##### 将稳定版grafana添加到源里面；tee命令是读取标准输入并输出到文件内，-a属性是添加到既有文件后面而非覆盖
```shell=
wget -q -O - https://packages.grafana.com/gpg.key | sudo apt-key add -
echo &#34;deb https://packages.grafana.com/oss/deb stable main&#34; | sudo tee -a /etc/apt/sources.list.d/grafana.list 
```
##### 启动服务并验证服务是否已启动
```shell=
sudo apt-get update
sudo apt-get install grafana
sudo systemctl daemon-reload
sudo systemctl start grafana-server 
sudo systemctl status grafana-server
```
##### 将 Grafana 服务器配置为在引导时启动
```shell=
sudo systemctl enable grafana-server
```
#### 配置Granfana
##### 登录Grafana
Grafana的UI管理页面默认监听端口是3000，第一次登录 Grafana，在浏览器里输入 http://localhost:3000 。Grafana默认登录的管理员账号密码都是：admin。需要修改端口等 Grafana 配置，修改配置文件server的相关配置即可。
##### 添加数据源

![](https://pad.degrowth.net/uploads/upload_38c89782c5f5990c89a71ba93003c0b2.png)
添加influxdb数据源，更改URL
![](https://pad.degrowth.net/uploads/upload_8eba6e2218658f127199375feca71ee3.png)
设置完成点击Save &amp; Test按钮
##### dashboard 配置
grafana 安装完了，根据自己需要要添加图表，图标有不同的展示形式，可以自定义，也可以导入你需要的仪表盘。grafana 官方提供了很多可供导入的图表：https://grafana.com/grafana/dashboards
单击标题面板可打开一个菜单框。单击edit 选项面板将会打开额外的配置选项。
![](https://pad.degrowth.net/uploads/upload_22051e34510f6c939ff75ddce50f2d0d.png)

添加图表
![](https://pad.degrowth.net/uploads/upload_c8d61c4618474f13c85a26d1e5e7c185.png)

添加光照图表，展示一天中不同时刻的光照数据。
![](https://pad.degrowth.net/uploads/upload_340de1227666edc8bb370b6fc890cd78.png)

添加温度图表，展示一天中不同时刻的温度情况。
![](https://pad.degrowth.net/uploads/upload_0d804ca05a5e484ede1edf9df0ae42e0.png)

添加最低温度图表，展示当天最低温度
![](https://pad.degrowth.net/uploads/upload_6eed13722cd0df0aed5929b36b08767a.png)

添加最高温度图表，展示当天最高温度
![](https://pad.degrowth.net/uploads/upload_9aded39a06f784b3b3c9ef3feb5e6a26.png)

添加平均温度图表，展示当天的平均温度。
![](https://pad.degrowth.net/uploads/upload_2179968ead85d6c2c8c32127cbfb7509.png)

添加平均气体浓度图表，展示一天中不同时刻的平均气体浓度。
![](https://pad.degrowth.net/uploads/upload_624ddab2504c98b0d2a394a588a30211.png)

添加火焰图表，展示火焰传感器的数据，监控是否发生火灾。
![](https://pad.degrowth.net/uploads/upload_11ccb5f9ccf925834d12a2ae07f17b1c.png)

最后记得保存。
![](https://pad.degrowth.net/uploads/upload_4c8918e3250bc86bcfc31f93763b21a5.png)

## 第八章 关系型数据库与人工智能应用
### （待补充）什么是关系型数据库，列举代表产品MySQL
### (待补充)什么是机器学习，列举典型应用，如人脸识别
   我们的计算机学院大楼物联网系统还要实现的一个功能是人脸识别，并将采集到的人脸图片存储到MySql数据库中。这一应用的部署是为了后期实现人物搜寻，输入人物的特征，如着装特征，相貌特征，然后搜寻人物的位置。
   人脸识别，是基于人的脸部特征信息进行身份识别的一种生物识别技术。通常采用摄像机或摄像头采集含有人脸的图像或视频流，并自动在图像中检测和跟踪人脸。人脸识别系统主要包括四个组成部分，分别为人脸图像采集及检测、人脸识别预处理、人脸图像特征提取以及匹配与识别。
  近年来，各种局部特征提取方法出现。为了避免输入的图像的高维数据，仅仅使用的局部特征描述图像的方法被提出，提取的特征(很有希望的)对于局部遮挡、光照变化、小样本等情况更强健。使用什么方法来提取时域空间的局部特征依旧是一个开放性的问题。
   前面我们将连接在STM32上的传感器的数据发送到了树莓派上，这一节我们的人脸识别也需要用到树莓派。我们需要将摄像头和雷达连接到树莓派上，雷达负责检测是否有人，摄像头负责拍摄人脸。摄像头通过软排线和树莓派相连。
  雷达用杜邦线进行连接，VCC和树莓派1号引脚相连，GND和树莓派6号引脚相连，OUT和树莓派GPIO引脚相连。（雷达的VCC模块接直流电3.3V，GND接GND，当有感应到移动物体时，模块OUT脚输出高电平，平时低电平）。
  树莓派的GPIO（GeneralPurposeI/OPorts）：意思为通用输入/输出端口，通俗地说，就是一些引脚，可以通过它们输出高低电平或者通过它们读入引脚的状态-是高电平或是低电平。GPIO是个比较重要的概念，用户可以通过GPIO口和硬件进行数据交互（如UART），控制硬件工作（如LED、蜂鸣器等），读取硬件的工作状态信号（如中断信号）等。GPIO口的使用非常广泛。掌握了GPIO，差不多相当于掌握了操作硬件的能力。
  ![](https://pad.degrowth.net/uploads/upload_f74bbcd3acbadb185b6fb0d5308ba516.png)
### 前导实验  MySQL数据库安装与使用
#### 实验目标
#### 实验步骤
##### 下载安装MySQL数据库
为了实现快速部署与应用并与前面的应用保持一致，我们将采用docker版本的MySQL
1. 拉取MySQL镜像

```shell
docker pull mysql:latest
```
2. 查看本地镜像

![](https://pad.degrowth.net/uploads/upload_49b9cde41f031decf7cfb0d8eb3c03c9.png)
从上图我们可以看到我们已经安装了最新版本的mysql镜像

3. 运行容器 
安装完成后我们可以使用以下命令来运行mysql容器：
```shell=
docker run -itd --name mysql-test -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
```
4. 安装成功

通过docker ps 命令查看是否安装成功
![](https://pad.degrowth.net/uploads/upload_6746bfd0ef9acb1812331bf5a5cf74b4.png)
本地可以用过root和密码123456访问MySQL服务。
![](https://pad.degrowth.net/uploads/upload_a5f7f3731924614d86ce80f876a68b3c.png)

5. 创建用于存储人脸图片数据的数据库
```shell=
create database test1；
```
使用show databases;可以看到数据库test1已经创建成功
![](https://pad.degrowth.net/uploads/upload_4032a66e59ae19388615e5264af227d2.png)
##### 将图片数据存储到数据库中
1. 安装pymysql，pymsql是从Python连接到MySQL数据库服务器的接口，能够使我们可以通过python程序操作数据库
    ```shell
    pip install pymysql
    ```
2. 连接到本地MySQL数据库
```python=
import pysql
class Database():
    def __init__(self):
        self.connection = pymysql.connect(host=&#39;localhost&#39;, user=&#39;root&#39;, port=3306, passwd=&#39;123456&#39;, db=&#39;test1&#39;,
                                          charset=&#39;utf8&#39;)
        #声明MySQL游标
        self.cursor = self.connection.cursor()
```
3. 读取图片，图片信息不能够直接进行存储，因此我们需要将图片信息以二进制格式进行读取
```python=
    with open(&#34;./test.jpg&#34;,&#34;rb&#34;) as file:
        image = file.read()
```
5. 创建数据库中存储图片的表,存放图片字段的属性为longblob，即long binary large object，最多可存放4M大小的图片。
```python
def create_image_table(self):
        sql = &#39;create table if not exists picture ( \
                image longblob);&#39;

        try:
            self.cursor.execute(sql)

            self.connection.commit()

        except pymysql.Error:
            print(pymysql.Error)

```
6. 存入MySQL
将二进制格式的图片数据存入MySQL
```python=
def insert_image(self, image):
    sql = &#34;insert into picture(image) values(%s)&#34;
    self.cursor.execute(sql, image)
    self.connection.commit()
```
7. 保存MySQL查询得到的图片数据存为图片
以二进制的格式写出图片
```python=
def get_image(self, path):
    sql = &#39;select * from picture&#39;
    try:
        self.cursor.execute(sql)
        image = self.cursor.fetchone()[0]
        with open(path, &#34;wb&#34;) as file:
            file.write(image)
    except pymysql.Error:
        print(pymysql.Error)
    except IOError:
        print(IOError)
```
完整的实现代码由以上关键代码稍稍扩展组成，相信同学们一定可以写出正确的代码的。
### 实验一 人脸识别
#### 实验目标
  我们要在树莓派上安装Opencv，使用OpenCV软件包中随附的LBPH（Local Binary Pattern Histograms本地二进制模式直方图）进行人脸识别。同时将雷达和摄像头连接到树莓派上，雷达检测到人以后，摄像头开始捕捉人脸。
#### 实验步骤
##### 安装opencv依赖项
1.为了加快后面下载速度，换国内源
（1）打开终端命令窗口
（2）然后在命令行窗口内输入 sudo nano /etc/apt/sources.list 并回车，进入编辑器。
然后把原来的内容注释掉，然后添加在上面添加以下内容：
```shell=
deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
```
保存退出
（3）sudo nano /etc/apt/sources.list.d/raspi.list 同样把文件内容替换，原来的内容注释掉。加入以下内容：
```shell=
deb http://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ buster main ui
```
保存退出
2.更新系统及源：
```shell=
sudo apt-get update &amp;&amp; sudo apt-get upgrade
```
3.切换Python版本
输入python3，进入Python交互环境查看自己是否有Python3，若无Python3，先安装:把Python2删掉，链接Python3。终端输入python查看Python版本
```shell=
sudo ln -s /usr/bin/python3.7 /usr/bin/python
```
4.安装开发工具：
```shell=
sudo apt-get install build-essential cmake unzip pkg-config
```
5.安装图像和视频库、安装GTK，GUI后端：
```shell=
sudo apt-get install libjpeg-dev libpng-dev libtiff-dev
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install libxvidcore-dev libx264-dev
```
6.安装免除GTK警告的包
```shell=
sudo apt-get install libcanberra-gtk*
```
7.下载OpenCV和OpenCV_contrib
下载OpenCV和OpenCV_contrib，注意：若安装的是OpenCV-4.3.0，则下载OpenCV_contrib时，必须也是OpenCV_contrib-4.3.0

OpenCV-4.3.0云盘下载链接:
链接: https://pan.baidu.com/s/1cLy7M2JSsLsEAsjqfXFsPg
提取码：zezp
opencv_contrib-4.3.0云盘下载链接: 链接: https://pan.baidu.com/s/1jFaBzr46SQkjo3E1ChbC2w
提取码：ru35

传输到树莓派的/home/pi目录下，对两个压缩包解压：
```shell=
cd
unzip opencv-4.3.0-openvino-2020.3.0.zip
unzip opencv_contrib-4.3.0.zip
```
将两个文件夹重命名为opencv和opencv_contrib：
```shell=
mv  opencv-4.3.0-openvino-2020.3.0  opencv
mv  opencv_contrib-4.3.0  opencv_contrib
```
##### 为OpenCV配置Python3虚拟环境
1.安装pip
```shell=
wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
```
2.安装虚拟环境
```shell=
sudo pip install virtualenv virtualenvwrapper
sudo rm -rf ~/get-pip.py ~/.cache/pip
```
3.打开~/.profile文件
```shell=
sudo nano ~/.profile
```
将以下行添加到~/.profile中
```shell=
export WORKON_HOME=/home/pi/.virtualenvs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export VIRTUALENVWRAPPER_VIRTUALENV=/usr/local/bin/virtualenv
source /usr/local/bin/virtualenvwrapper.sh
export VIRTUALENVWRAPPER_ENV_BIN_DIR=bin
```
重新加载：
```shell=
source ~/.profile
```
4.下载numpy：
```shell=
pip install numpy
```
##### 编译OpenCV
安装cmake
```shell=
sudo apt-get install cmake
cd ~/opencv
mkdir build
cd build
```
运行CMake来配置OpenCV 4
```shell=
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib/modules \
    -D ENABLE_NEON=ON \
    -D ENABLE_VFPV3=ON \
    -D BUILD_TESTS=OFF \
    -D OPENCV_ENABLE_NONFREE=ON \
    -D INSTALL_PYTHON_EXAMPLES=ON  \
    -D BUILD_EXAMPLES=ON  ..
```
配置成功后开始编译
```shell=
sudo make -j4
```
注：编译遇错导致编译中止：fatal error: boostdesc_bgm.i: No such file or directory
解决办法：下载所有文件（不是文件夹）复制到”opencv_contrib/modules/xfeatures2d/src/”下
百度云链接: https://pan.baidu.com/s/1T0m0LPdstoTpEd2s8WUHkA
提取码：hlms

继续编译：sudo make -j4 基本只有上面一个错误，如果还出现其他错误，继续编译一两次直到完全成功就OK了

安装与检查
```shell=
sudo make install
```
进入Python交互环境
```shell=
Python3
import cv2
```
无错提示则说明opencv安装成功
```shell=
from cv2 import face
```
无错提示则说明opencv_contrib安装成功
Ctrl+D退出Python交互环境
##### 打开摄像头
终端输入 
```shell=
sudo raspi-config
```
选择 Interface Options &gt; Camera &gt;Yes &gt; OK &gt;Finish &gt;重启（reboot）

我们使用luvcview这个工具来查看摄像头的实时视频
利用which luvcview这个命令来查看是否已安装这个工具，正常情况会返回“/usr/bin/luvcview”
若没有返回任何信息，则使用 sudo apt-get install luvcview 来安装工具。
安装完成后使用luvcview -s 1080x720来启动该工具，其中1080x720代表采集的分辨率
```shell=
which luvcview
sudo apt-get install luvcview
luvcview -s 1080x720
```
![](https://pad.degrowth.net/uploads/upload_2864347b3fd89d2358d11fe123e0cd2c.png)

Ctrl+C退出摄像
##### 摄像头检测
下载相关程序
```shell=
cd
git clone https://gitee.com/jeebus/Facial_Recognition.git
cd Facial_Recognition
```
摄像头检测
```shell=
python3 simpleCamTest.py
```
若出现两个窗口，一个色彩一个灰色证明摄像头正常，摄像是倒过来的，要改的话下面再改，或者直接把摄像头倒置。
可在命令行窗口按下 [Ctrl]+C 组合键退出
##### 人脸检测
人脸检测算法：
基于Haar的脸部检测器的基本思想是，对于面部正面大部分区域而言，会有眼睛所在区域应该比前额和脸颊更暗，嘴巴应该比脸颊更暗等情形。它通常执行大约20个这样的比较来决定所检测的对象是否为人脸，实际上经常会做上千次。
基于LBP的人脸检测器基本思想与基于Haar的人脸检测器类似，但它比较的是像素亮度直方图，例如，边缘、角落和平坦区域的直方图。
这两种人脸检测器可通过训练大的图像集找到人脸，这些图像集在opencv中存在XML文件中以便后续使用。
```shell=
cd FaceDetection
python3 faceDetection.py
```
![](https://pad.degrowth.net/uploads/upload_58b3cdef85e19c34c1feb4c5b30166e1.png)

摄像已经倒置了，在faceDetection.py文件里面把代码改一下
进入文件
```shell=
nano faceDetection.py
```
把括号里的-1改成1
![](https://pad.degrowth.net/uploads/upload_d1ee9b08935d9218f300142b505b00e1.png)
再次运行faceDetection.py文件，摄像就正常了
```shell=
python3 faceDetection.py
```
运行人脸检测程序，把你的人脸放在摄像头前，人脸上出现方框检测成功
可点击窗口，按下Esc退出

##### 人脸识别
人脸识别算法：
1、特征脸，也称PCA（主成分分析）
2、Fisher脸，也称LDA（线性判别分析）
3、局部二值模式直方图（Local Binary Pattern Histograms，LBPH）
这些方法都有类似的一个过程，即先对数据集进行训练，对图像或视频中的人脸进行分析，并且从两个方面确定：1.是否识别到对应的目标，2.识别到的目标的置信度，在实际中通过阈值进行筛选，置信度高于阈值的人脸将被丢弃
LBPH 是利用局部二值模式直方图的人脸识别算法,LBP 是典型的二值特征描述子，更多的是整数计算，而整数计算的优势是可以通过各种逻辑操作来进行优化，因此效率较高。另外通常光照对图中的物件带来的影响是全局的，也就是说照片中的物体明暗程度，是往同一个方向改变的，可能是全部变亮或全部变暗，因此 LBP 特征对光照具有比较好的鲁棒性。 (详细介绍)
OpenCV提供了CV::Algorithm类，该类有几种不同的算法，用其中一种算法就可以完成简单而通用的人脸识别。
OpenCV的contrib模板中有一个FaceRecognizer类，它实现以上这些人脸识别算法。

1.收集人脸数据
```shell=
cd …/FacialRecognition
mkdir dataset
```
摄像倒置的，先进文件修改代码，进入文件：
```shell=
nano 01_face_dataset.py
把括号里的-1改成1
```
保存退出
```shell=
python3 01_face_dataset.py
```
显示输入此用户ID，输入1 &gt; 然后人脸对着摄像头，程序会收集30个样本数据 &gt; 样本数据在dataset中，可在用户界面直接打开查看
![](https://pad.degrowth.net/uploads/upload_43aba4a1906e61fb9e406b10e8ea4c4b.png)

2.训练
```shell=
mkdir trainer
python3 02_face_training.py
```
3.识别
同上，摄像倒置的，先修改一下代码，没有的直接运行
```shell=
python3 03_face_recognition.py
nano 03_face_recognition.py
```
保存退出
```shell=
python3 03_face_recognition.py
```
人脸对着摄像头，查看是否识别成功
![](https://pad.degrowth.net/uploads/upload_fe14e18946a89b55dc2617348ad28287.png)


### 实验二 口罩检测
#### 实验原理
疫情之下，口罩成了人们出门必备。我们想要借助树莓派和摄像头实现检测人物是否佩戴口罩。这样保安或者门卫不用一直盯着进出的人，在值班室也可以及时知道有人没有戴好口罩，方便他们对没有佩戴口罩的人进行提醒。

树莓派不适合训练模型，但是我们可以把已经训练好的模型拿到上面去跑。我们使用AIZOO团队实现的口罩检测项目，使用的是目标检测常用的SSD算法。
AIZOO团队给出的代码在Windows上很容易以跑通。安装opencv和kears比较容易。也不需要很复杂的更改代码。但是opencv以及kears 这两个库在树莓派上比较难以安装。我们在人脸识别那部分已经安装好了opencv，现在还需要在树莓派的环境中安装TensorFlow和keras。项目链接：https://github.com/AIZOOTech/FaceMaskDetection

卷积神经网络，正是由于卷积操作而得名。它进一步减少了参数，并且具有一定的平移、旋转、拉伸不变性。解决了全连接DNN的结构里下层神经元和所有上层神经元都能够形成连接，带来的参数数量膨胀的问题。
对于CNN来说，并不是所有上下层神经元都能直接相连，而是通过“卷积核”作为中介。同一个卷积核在所有图像内是共享的，图像通过卷积操作后仍然保留原先的位置关系。两层之间的卷积传输的示意图如下：
![](https://pad.degrowth.net/uploads/upload_0ba8790670bddd863f8cb0e99af1d6d2.png)

卷积神经网络CNN的结构一般包含这几个层：
* 输入层：用于数据的输入
* 卷积层：使用卷积核进行特征提取和特征映射
* 激励层：由于卷积也是一种线性运算，因此需要增加非线性映射
* 池化层（汇合层）：进行下采样，对特征图稀疏处理，减少数据运算量。
* 全连接层：通常在CNN的尾部进行重新拟合，减少特征信息的损失
卷积层和汇合层交替连接，最后一层卷积层连接几层全连接层（有点像DNN的隐含层，而且是全连接的），然后连接到输出层。

其实，基于深度学习的人脸检测算法，多数都是基于深度学习目标检测算法进行的改进，或者说是把通用的目标检测模型，为适应人脸检测任务而进行的特定配置。而众多的目标检测模型（Faster RCNN、SSD、YOLO）中，人脸检测算法最常用的是SSD算法，例如知名的SSH模型、S3FD模型、RetinaFace算法，都是受SSD算法的启发，或者基于SSD进行的任务定制化改进， 例如 将定位层提到更靠前的位置，Anchor大小调整、Anchor标签分配规则的调整，在SSD基础上加入FPN等。
AIZOO团队用到的就是SSD 算法，主要思路是均匀地在图片的不同位置进行密集抽样，抽样时可以采用不同尺度和长宽比，然后利用CNN提取特征后直接进行分类与回归，整个过程只需要一步，所以其优势是速度快。缺点是精度不够高。
SSD算法采用的是基于回归的模式(类似于YOLO)，在一个网络中直接回归出物体的类别和位置，检测速度很快。同时也利用了基于区域的概念(类似于Faster RCNN)，在检测的过程中，使用了许多候选区域作为ROI。
SSD的骨干网络是基于传统的图像分类网络，经过卷积层(con. layer)和池化层(max pooling)的处理得到特征图 (feature map)。再在这个特征图上进行回归，得到物体的位置和类别。

注：ROI（region of interest），感兴趣区域。机器视觉、图像处理中，从被处理的图像以方框、圆、椭圆、不规则多边形等方式勾勒出需要处理的区域，称为感兴趣区域，ROI。
#### 实验目标
在树莓派上安装安装TensorFlow和keras，实现基于目标检测SSD算法实现的树莓派口罩检测。
#### 实验步骤
##### 安装依赖
现在输入python会默认进入python2.7。可以用下面的代码将树莓派中的python的链接删掉，重新链接到python3，这样后面可以直接使python命令直接进入python环境中，也可以省下安装其他python包的时候出现安装失误的情况。
```shell=
sudo rm /usr/bin/python
sudo ln -s /usr/bin/python3.7 /usr/bin/python
```
验证：输入python，进入python3.7的环境
![](https://pad.degrowth.net/uploads/upload_2aff413bf358b282b70c6741af7d0f3b.png)
更新软件
```shell=
sudo apt-get upgrade
```
因为后面可能还会有非常多的库因为网络原因安装不上，所以我们通过软件对树莓派/home/pi目录下面新建一个文件夹，进行安装用。例如我就新建了一个tensorflow文件夹（建议用英文）

网卡导致的下载终止，如果多次重试都失败我们可以用电脑进入它提供的网址，在电脑端下载然后传入树莓派进行安装。

安装依赖
有的可能会很慢，一般是因为清华镜像源没有在国外下载，出现进度条卡住同时大量红字报错是因为网卡导致的下载终止，这时候重新输入一下下载命令就可以了
```shell=
sudo apt-get install -y libhdf5-dev libc-ares-dev libeigen3-dev

python3 -m pip install keras_applications==1.0.8 --no-deps

python3 -m pip install keras_preprocessing==1.1.0 --no-deps
```
![](https://pad.degrowth.net/uploads/upload_925d9fd50209fb2bc5380915d3a70725.png)
安装h5py。
之前我下载了最新版本，安装TensorFlow时便出错了，是依赖的时候没有指定版本的时候造成的。而我需要的是较低版本的，选择哪个版本很重要，所以环境搭配选择版本还是要按官方的来。从下面这个链接中下载h5py-2.10.0。
https://www.piwheels.org/simple/h5py/h5py-2.10.0-cp37-cp37m-linux_armv7l.whl
安装包导入树莓派用下面的命令进行安装，这时候命令中要用导入安装包的全名h5py-2.10.0，不能只用h5py，不然树莓派找不到
```shell=
python3 -m pip install  h5py-2.10.0-cp37-cp37m-linux_armv7l.whl
```
![](https://pad.degrowth.net/uploads/upload_4ffb725d1b277991486656cd73c5ea20.png)
然后继续下面的依赖环境的安装，因为网络原因卡住了就用上面的手动导入安装包的方法进行安装。
```shell=
sudo apt-get install -y openmpi-bin libopenmpi-dev

sudo apt-get install -y libatlas-base-dev

python3 -m pip install -U six wheel mock

sudo apt-get install libhdf5-dev

sudo apt-get install python-h5py
```
此时我们还缺少一个依赖库scipy，而且这个库清华镜像站也没有，我选择手动导入安装包，这里我们进入这个网址
https://www.piwheels.org/simple/scipy/scipy-1.4.1-cp37-cp37m-linux_armv7l.whl
进行下载然后导入树莓派
![](https://pad.degrowth.net/uploads/upload_3170fe64caaf6ca6c6df3b05d5764622.png)

##### 安装TensorFlow
安装完依赖环境我们就可以安装tensorflow了，因为我们在python3.7环境下，所以我们安装tensorflow2.4.0。

下载TensorFlow的安装包此步骤可以直接pip下载安装，但是限速网络等原因会多次不成功。建议下载安装包，传到树莓派上再进行安装。下载地址：
https://github.com/lhelontra/tensorflow-on-arm/releases
找到对应的版本

中间的CPXX，代表python版本，选错了无法安装。例如python37，应该选择CP37，pytho2.7就选择CP27，如果是pi1或者pizero（包括zerow），选择结尾是armv6l的；如果是树莓派2/3/4就选择结尾是armv7l.whl的。（可使用命令 uname -a 查看系统信息，因为我们是树莓派4b所以选择了如下这个
![](https://pad.degrowth.net/uploads/upload_3c1cc2dd2b091a3c125dbd5ba3b3dd1a.png)
同样我们安装的时候需要进入对应目录（我的是tensorflow）然后安装的时候输入安装包的全名。
进入我的存放安装包的目录
```shell=
cd tensorflow
```
输入 ls，可以看到文件夹里面有什么 
![](https://pad.degrowth.net/uploads/upload_9be93153057b5bc32f536c4c47da2b73.png)
之前的安装包和要的tensorflow2.4.0安装包都在里面，然后开始tensorflow2.4.0的安装(记得这里是输入安装包全名)
```shell=
 sudo pip3 install tensorflow-2.4.0-CP37-none-linux_armv7l.whl
```
然后就交给网速，大概顺利会安装很快，导入过程会有warning提示，使用正常，可能是版本升级后有些函数使用改变。正常的话，安装会出现一大片红色error，大概率是网络问题，多试试。

验证是否安装完成
```shell=
pip show tensorflow
```
![](https://pad.degrowth.net/uploads/upload_1a4a14776eab4b3cf80b6ce9a157e4b1.png)
##### 安装keras
安装keras需要在TensorFlow的基础上，稍微了解的就知道了，keras支持两个背后的驱动，其中之一是TensorFlow，keras本身就是一种高级包装。

我之前用命令sudo pip install keras安装了最新版keras，后面出现了错误，因为安装错了版本。用下面的命令安装2.3.1版本的keras，如果再次出现卡住，大量红字就再试一次。这是清华镜像的地址，没必要手动安装，一般是网卡。
```shell=
sudo pip install keras==2.3.1
```
##### 口罩检测
AIZOOTech的项目支持5大主流深度学习框架（PyTorch、TensorFlow、MXNet、Keras和Caffe），已经写好接口了；可以根据自身的环境选择合适的框架，比如：TensorFlow；所有模型都在models文件夹下。
参考该项目的tensorflow_infer.py代码，编写运行文件pi_cam.py，使该项目可以更好的在树莓派上运行。关键代码示例：

引入项目中的模块和一些库
```shell=
import cv2
import numpy as np
from keras.models import model_from_json
from anchor_generator import generate_anchors
from anchor_decode import decode_bbox
from nms import single_class_non_max_suppression
model = model_from_json(open(&#39;models/face_mask_detection.json&#39;).read())
model.load_weights(&#39;models/face_mask_detection.hdf5&#39;)
```
训练目标检测模型，最重要的合理的设置anchor的大小和宽高比，一般是统计数据集的目标物体的宽高比和大小来设置anchor的大小和宽高比。
例如，AIZOOTech团队在标注的口罩人脸数据集上，读取了所有人脸的标注信息，并计算每个人脸高度与宽度的比值，统计得到高度与宽比的分布直方图。因为人脸的一般是长方形的，而很多图片是比较宽的，例如16:9的图片，人脸的宽度和高度归一化后，有很多图片的高度是宽度的2倍甚至更大。归一化后的人脸高宽比集中在1~2.5之间。所以，根据数据的分布，将五个定位层的anchor的宽高比统一设置为1，0.62, 0.42。（转换为高宽比，也就是约1，1.6：1，2.4:1）

注：目标检测之anchor
anchor字面意思是锚，anchor在计算机视觉中有锚点或锚框，目标检测中常出现的anchor box是锚框，表示固定的参考框。
```shell=
# anchor configuration
feature_map_sizes = [[33, 33], [17, 17], [9, 9], [5, 5], [3, 3]]
anchor_sizes = [[0.04, 0.056], [0.08, 0.11], [0.16, 0.22], [0.32, 0.45], [0.64, 0.72]]
anchor_ratios = [[1, 0.62, 0.42]] * 5
# generate anchors
anchors = generate_anchors(feature_map_sizes, anchor_sizes, anchor_ratios)
anchors_exp = np.expand_dims(anchors, axis=0)
#axis=0表示按列 取最大值，如果axis=1表示按行 取最大值， axis=None表示全部
# 检测推理
id2class = {0: &#39;Mask&#39;, 1: &#39;NoMask&#39;}
def inference(image, #3D numpy图⽚数组
              conf_thresh=0.5, # 分类概率的最⼩阈值
              iou_thresh=0.4, # ⽹管的IOU门限
              target_shape=(160, 160), #模型输⼊⼤⼩
              draw_result=True, #是否将边框拖⼊图像
              show_result=True #是否显⽰图像
              ):
    # image = np.copy(image)
    output_info = []
    height, width, _ = image.shape
    image_resized = cv2.resize(image, target_shape)
    image_np = image_resized / 255.0  # 归一化到0~1
    image_exp = np.expand_dims(image_np, axis=0)

    result = model.predict(image_exp)

    y_bboxes_output = result[0]
    y_cls_output = result[1]

    # remove the batch dimension, for batch is always 1 for inference.
    y_bboxes = decode_bbox(anchors_exp, y_bboxes_output)[0]
    y_cls = y_cls_output[0]
```

后处理部分主要就是非最大抑制（NMS），使用单类的NMS，也就是戴口罩人脸和不戴口罩人脸两个类别一起做NMS，从而提高速度。

注：非最大抑制 NMS（non maximum suppression）顾名思义抑制最大值之外的元素，可以理解为局部最优。在进行目标检测时一般会采取窗口滑动的方式，在图像上生成很多的候选框，然后把这些候选框进行特征提取后送入分类器，一般会得出一个得分（score），比如人脸检测，会在很多框上都有得分，然后把这些得分全部排序。选取得分最高的那个框，接下来计算其他的框与当前框的重合程度（iou），如果重合程度大于一定阈值就删除，因为在同一个脸上可能会有好几个高得分的框，都是人脸但是不需要那么框我们只需要一个就够了。
如果图片中有好几个人脸，你这选取一个最大的，那第二个人脸怎么办呢。实际上这是一个迭代的过程，第一步的非极大值抑制就是选取了某一个最大的得分，然后删除了他周边的几个框，第二次迭代的时候在剩下的框里面选取一个最大的，然后再删除它周围iou区域大于一定阈值的，这样不停的迭代下去就会得到所有想要找到的目标物体的区域。

```shell=
    bbox_max_scores = np.max(y_cls, axis=1)
    bbox_max_score_classes = np.argmax(y_cls, axis=1)

# keep_idx是nms之后的活动边界框。
    keep_idxs = single_class_non_max_suppression(y_bboxes,
                                                    bbox_max_scores,
                                                    conf_thresh=conf_thresh,
                                                    iou_thresh=iou_thresh,
                                                    )

    for idx in keep_idxs:
        class_id = bbox_max_score_classes[idx]
        if class_id == 1:
            print(&#39;未带口罩了&#39;)
           
        else:
            print(&#39;戴口罩&#39;)

    return output_info
```
对视频流的处理
```shell=
def run_on_video(video_path, output_video_name, conf_thresh):
    cap = cv2.VideoCapture(video_path) #传入视频的路径
    if not cap.isOpened():
        raise ValueError(&#34;Video open failed.&#34;)
        return
    status = True
    print(&#39;detection is begining&#39;)
    img_id = 0
    while status:
        
        if img_id%7 == 0:#隔七秒拍一张图片
            print(img_id)
            status, img_raw = cap.read()
            img_raw = cv2.cvtColor(img_raw, cv2.COLOR_BGR2RGB)

            if (status):
                inference(img_raw,
                                 conf_thresh,
                                 iou_thresh=0.5,
                                 target_shape=(260, 260),
                                 draw_result=True,
                                 show_result=False)

        img_id = img_id +1
if __name__ == &#34;__main__&#34;:
    run_on_video(0, &#39;&#39;, conf_thresh=0.5)

```
将AIZOO团队提供的项目下载下来。以及我们编写的pi_cam.py放入同一目录下，上传到树莓派中（按照上面步骤 配置好环境 ）。命令行进入目录直接调用就可以。（如果python默认是python2.7 则要使用python3 pi_cam.py，不过我们前面已经把python命令默认链接为python3，所以只需输入python即可）
```shell=
cd tensorflow
cd FaceMaskDetection-master
python pi_cam.py
```
![](https://pad.degrowth.net/uploads/upload_0ff0ce6b9e1115fa0ff70456d1d3352c.png)
运行命令时出现问题  ImportError: cannot import name &#39;get_config&#39; from &#39;tensorflow.python.eager.context&#39;
![](https://pad.degrowth.net/uploads/upload_9869a6a8a96c59ce6c8d89f12f4a90d8.png)

安装这个
```shell=
pip install git+https://www.github.com/keras-team/keras-contrib.git 
```
上面这个命令如果执行响应时间过长，可采取如下操作：
设置http代理，git clone仓库
```shell=
git clone https://www.github.com/keras-team/keras-contrib.git 
```
然后进入keras-contrib 
```shell=
cd keras-contrib 
```
进行编译安装
```shell=
python setup.py build
python setup.py install 
```
![](https://pad.degrowth.net/uploads/upload_9e36917031f215991b4863e8730fe8ef.png)
最后便可以成功检测到是否佩戴口罩了
![](https://pad.degrowth.net/uploads/upload_5b629a999893cd58af262cdcbbcea911.png)

## 第九章 定位应用
  我们在室内部署好节点以后，如果能够知道节点的具体位置，可以帮助我们更好的管理我们的系统。因此，这一章将会介绍如何在节点上部署定位应用，要部署定位应用，首先要选择一种方案，目前GPS定位系统应用非常的广泛，但是在室内定位方面，由于GPS系统是通过无线电发射信号，会受到很多信号影响，比如电波，高楼等，GPS会受到墙体等阻碍、反射，所以不能直接进行室内定位。用GPS进行室内定位效果不是非常理想。那么现在通常如何来进行室内定位呢？随着wifi技术的广泛应用，目前很多室内区域都已经覆盖了wifi，所以wifi室内定位技术应用的非常广泛，下面我们就来介绍wifi室内定位技术。
### 实验一 wifi室内定位实验


## 第十章 Docker容器与K3S部署应用实验（引入K3S和docker，用influxDB和mqtt的容器化部署作为应用部署实践、以及数据聚合实验）
#### 实验目标
   同学们注意到我们使用了拥有一定算力的树莓派作为节点，那么我们要将树莓派的算力利用起来，在树莓派上部署一些应用，例如人脸识别、目标追踪、数据聚合等，但经过前面不少实验的实操我们可以发现，应用的部署会大大消耗我们有限的时间，应用对于树莓派的资源占用也是相当可观，并且一旦需要迁移结点，环境迁移的成本相当高，为解决此类问题，容器技术被提了出来，容器是一种沙盒技术，主要目的是为了将应用运行在其中，与外界隔离；及方便这个沙盒可以被转移到其它宿主机器，通俗点的理解就是一个装应用软件的箱子，箱子里面有软件运行所需的依赖库和配置。开发人员可以把这个箱子搬到任何机器上，且不影响里面软件的运行。为什么要使用容器可以通过以下两个图做对比。
   ![](https://pad.degrowth.net/uploads/upload_e42c99c9742396fe4d751a75a953e09c.png)
传统的应用部署方式是通过插件或脚本来安装应用。这样做的缺点是应用的运行、配置、管理、所有生存周期将与当前操作系统绑定，这样做并不利于应用的升级更新/回滚等操作，当然也可以通过创建虚机的方式来实现某些功能，但是虚拟机非常重，并不利于可移植性。新的方式是通过部署容器方式实现，每个容器之间互相隔离，每个容器有自己的文件系统 ，容器之间进程不会相互影响，能区分计算资源。相对于虚拟机，容器能快速部署，由于容器与底层设施、机器文件系统解耦的，所以它能在不同云、不同版本操作系统间进行迁移。容器占用资源少、部署快，每个应用可以被打包成一个容器镜像，每个应用与容器间成一对一关系也使容器有更大优势，使用容器可以在build或release 的阶段，为应用创建容器镜像，因为每个应用不需要与其余的应用堆栈组合，也不依赖于生产环境基础结构，这使得从研发到测试、生产能提供一致环境。类似地，容器比虚机轻量、更“透明”，这更便于监控和管理。
![](https://pad.degrowth.net/uploads/upload_ab262dca4210733226124873e66b8f75.png)
随着容器技术的不断发展，Docker横空出世，Docker不等同于容器，它是基于容器技术开发的一个应用，docker包含了运行一个应用程序所需要的所有东西，包括：代码、运行时、各种依赖和配置。那么这里面的代码、依赖、配置就是docker镜像包含的内容，而运行镜像里的内容则是容器做的事情，也就是运行。docker使用起来简单方便，解决了绝大多数用户需求。其它容器或多或少存在打包不方便、兼容性差等问题。而docker的方案中，不仅打包了本地应用程序，同时将本地环境（操作系统一部分）一起打包，实现本地与服务器的环境完全一致，做到了真正的一次开发随处运行。在我们实验中，influxDB数据库和MQTT服务器等应用都已经被容器化，因此，我们更愿意采用将应用进行docker部署。
#### 实验一、Docker容器的部署使用实验
#### 实验目标 
学会docker容器安装，了解docker使用方法
#### 实验一 docker容器部署使用
在linux系统上进行docker容器部署
参考部署命令
```shell=
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg2 \
    lsb-release \
    software-properties-common
    
curl -fsSL https://get.docker.com -o get-docker.sh

sudo sh get-docker.sh
```
设置开机启动
```shell=
sudo systemctl daemon-reload

sudo systemctl restart docker
```
安装完毕用 docker -v 测试是否安装成功
自此，docker安装成功，我们简单介绍一下docker的基本使用
常规命令
启动 Docker
```shell=
sudo systemctl start docker
```
停止 Docker
```shell=
sudo systemctl stop docker
```
重启 Docker
```shell=
sudo systemctl restart docker
```
修改配置后重启 Docker

```shell=
sudo systemctl daemon-reload
sudo systemctl restart docker
```
查看版本
```shell=
docker version
```
查看Docker 信息
```shell=
docker info
```
Docker 帮助
```shell=
docker --help
```
镜像命令
查看Docker上已经安装的镜像
```shell=
docker images
```
搜索Docker hub上面的镜像

```shell=
# 以tomcat为例
docker search tomcat
```
下载镜像
```shell=
以下载tomcat为例
docker pull tomcat[:version]
```
删除镜像
```shell=
# 以删除tomcat为例
docker rmi tomcat[:version]
# 通过镜像ID删除
docker rmi -f 镜像ID
# 通过镜像ID删除多个
docker rmi -f 镜像名1:TAG 镜像名2:TAG 
# 删除全部
# docker images -qa : 获取所有镜像ID
docker rmi -f $(docker images -qa)
```
容器命令
启动容器
```shell=
docker run [options] image [command] [arg...]
常用参数：

-d: 后台运行容器,并返回容器ID

-i: 以交互式运行容器,通常与-t同时使用

-p: 端口映射,格式为 主机(宿主)端口:容器端口

-t: 为容器重新分配一个伪输入终端,通常与-i同时使用

--name=&#34;name&#34;: 为容器指定一个名称

--expose=[]:开放一个端口或一组端口,宿主机使用随机端口映射到开放的端口
实例：
docker run --name mynginx -d nginx:latest
```
映射多个端口
```shell=
docker run -p 80:80/tcp -p 90:90 -v /data:/data -d nginx:latest
```
查看正在运行的Docker 容器
```shell=
docker ps
常用参数：

# 显示所有容器，包括当前没有运行的容器
-a
# 显示最近创建的容器
-l
# 显示最近创建的N个容器
-n
# 静默模式,只显示容器ID
-q
# 不截断输出
--no-trunc
```
退出容器
```shell=
# 退出并停止
exit
# 容器不停止退出
ctrl+P+Q
```
启动容器
```shell=
docker start 容器ID或容器name
```
重启容器
```shell=
docker restart 容器ID或容器name
```
停止容器
```shell=
docker stop 容器ID或容器name
```
强制停止容器
```shell=
docker kill 容器ID或容器name
```
删除容器
```shell=
# 删除已经停止的容器
docker rm 容器ID或容器name 
# 强制删除已经停止或正在运行的容器
docker rm -f  容器ID或容器name 
#一次性删除所有正在运行的容器
docker rm -f $(docker ps -qa)
```
#### 实验二、Kubernetes的部署与使用实验
#### 实验目标 
在实验一中我们引入了docker容器，但容器就相当于鸡蛋一般，虽然轻便，但一旦容器部署数量逐步上升，就像鸡蛋太多需要一个篮子来装一样，这时候Kubenets就充当了篮子的身份，Kubernetes的名字来自希腊语，意思是“舵手” 或 “领航员”。K8s是将8个字母“ubernete”替换为“8”的缩写,它是容器集群管理系统，是一个开源的平台，可以实现容器集群的自动化部署、自动扩缩容、维护等功能,我们用kubernetes去管理Docker集群，即可以将Docker看成Kubernetes内部使用的低级别组件。由于k8s对机器的最低运行要求比较高，各个结点的内存配置最少需要2G，对于树莓派来讲，它本身就属于边缘设备，性能方面不是很突出，不少结点在运行k8s时会出现启动失败，不断进行重新启动代理的问题，导致整个集群无法正常运作。因此在本实验中，我们选取安装轻量级的kubenets，k3s。k3s是一个高可用的、经过CNCF认证的Kubernetes发行版，专为无人值守、资源受限、偏远地区或物联网设备内部的生产工作负载而设计，完美适配边缘环境，简单且安全。
#### 实验内容
在linux系统上进行kubenets的轻量级应用k3s的部署使用
#### 参考示例命令
##### 1.关闭swap分区
1.永久关闭

``` shell=
sudo sed -ri &#39;s/.*swap.*/#&amp;/&#39; /etc/fstab 
```

2.暂时关闭

```shell=
sudo swapoff -a
```

3.对于树莓派原生操作的话应该没有单独的swap分为，故上诉两种操作可能在重启后还是无法关闭swap分区，故采用第三种方法(开机自动执行)。

```shell=
sudo vim /etc/rc.local
在文件 exit 0 之前添加
sudo swapoff -a
```

**用free -m 查看swap分区是否关闭**

配置iptables参数，使得流经网桥的流量也经过iptables/netfilter防火墙

```shell=
sudo tee /etc/sysctl.d/k8s.conf &lt;&lt;-&#39;EOF&#39;
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
 
sudo sysctl --system
```
##### 配置docker
```shell=
sudo vi /etc/docker/daemon.json
```

输入以下内容

```shelll=

{
  &#34;registry-mirrors&#34;: [
    &#34;https://dockerhub.azk8s.cn&#34;,
    &#34;https://reg-mirror.qiniu.com&#34;,
    &#34;https://quay-mirror.qiniu.com&#34;
  ],
  &#34;exec-opts&#34;: [ &#34;native.cgroupdriver=systemd&#34; ]
}

```

保存后重启docker

```shell=
sudo systemctl daemon-reload

sudo systemctl restart docker
```
#### 2.让docker不操作iptables

```shell=
sudo vim /etc/docker/daemon.json
```

将如下内容加入文件中

```shell=
{
    &#34;iptables&#34;: false
}
```

#### 3.开启memory的cgroup功能

``` shell=
sudo vim /boot/cmdline.txt
```

在打开文本行的末尾添加此文本，但不要创建任何新行

```shell=
cgroup_enable=memory
```

重启

``` shell=
sudo reboot
```

#### 4.修改主机名称(master和worker结点最好都改一下)

```shell=
sudo hostnamectl set-hostname k8s-node01(主机名称或者工作结点名称)
```

#### 5.master安装k3s

先将worker结点的ip和主机名称加入到master的 /etc/hosts文件中去(本地组网情况下ip请填写组网ip)
```shell=
#文本最后添加
192.168.66.20 k8s-node01
192.168.66.21 k8s-node02
192.168.66.10 k8s-master01
```
执行安装命令
```shell=
curl -sfL http://rancher-mirror.cnrancher.com/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn sh -

```
卸载命令
```shell=
/usr/local/bin/k3s-uninstall.sh
```

查看加入令牌

```shell=
sudo cat /var/lib/rancher/k3s/server/node-token
```

#### 6.node安装k3s(需要连接互联网才能执行)

```shell=
curl -sfL http://rancher-mirror.cnrancher.com/k3s/k3s-install.sh | INSTALL_K3S_MIRROR=cn K3S_URL=https://myserver(master ip):6443 K3S_TOKEN=mynodetoken(加入令牌) sh -

#安装完成后用systemctl status k3s-agent查看运行状态，如果等了几分钟后还是未启动成功建议卸载重新安装
#卸载命令 
/usr/local/bin/k3s-agent-uninstall.sh
```
上诉操作都需要在联网的环境下进行，在后续采取本地组网的情况下，树莓派的将不会在连接互联网，因此我们需要手动为worker结点添加到master的默认路由，否则会导致错误(不采用组网方式请略过).
```shell=
#执行开机执行添加默认路由
sudo vim /etc/rc.local
#在文件exit 0 之前添加
sudo route add default gw 10.0.0.3(mater组网ip)
```
#### 7.验证搭建成功
```shell=
#master结点执行
sudo kubectl get nodes
```
#### 8.优化前期工作
现在我们已经拥有docker了，因此，将前期的部署都实现容器化部署吧！(例如mqtt服务器容器化部署等)
## 第十一章 Web应用设计与开发
前面我们已经部署好了传感器，摄像头。也安装配置了Grafana来掌控系统的运行情况。为了开发出真正满足用户需求的系统，我们要开发出一个完整的web应用，这一过程我们将采用软件工程的完整流程。
首先我们要进行需求分析，需求分析完成后可以开始设计前端界面了，在这里，我们用Figma设计前端界面原型图。设计好原型图后根据原型图设计接口文档，接口文档设计完成后，我们可以根据接口文档进行后端开发。
本次开发中，前端框架使用Vue，开发语言使用JavaScript、Html5、CSS。后端框架使用SpringBoot，后端开发语言使用Java、嵌入式Python。前后端框架IDE均使用IDEA。前端可根据原型图下载相近的模板，在模板基础上进行修改，可提高效率。
#### 需求分析
前端展示 
在前端页面上显示计算机学院大楼的平面图，还要显示数据库中的数据，如传感器IP、传感器类型、读数、摄像头等。
还需实现用户登录，寻人，部署应用，Grafana系统监测等功能。

前端交互 
这里的前端交互，就是浏览器上的JavaScript交互，通过POST,GET等http协议，与服务端进行同步或者异步数据交互。比如登录，后台根据用户在前端输入的指令在相应节点部署应用等等功能。

后台功能 
对支撑整站需要用到的数据，进行管理维护。比如用户使用寻人功能时，后台存储用户上传的照片、人物信息。对用户进行人脸识别时，后台数据库存储用户的照片等等。

#### 前端需求：
标题： 计算机学院大楼环境物联网系统
共三个页面：主页面，应用管理，目标搜索，按钮放置在主页面的上方，标题的下方.主页面的右侧为登录，各个页面具体需求如下：
登录：
主页展示界面： 展示二楼的平面图，并在图上标识出每个节点，鼠标指针移动到节点上可以看见节点的简略信息（ip等），点击节点跳转到新的页面，页面展示节点的具体信息，例如传感器类型、读数、摄像头等。右边为登录界面。
  
目标搜寻按钮： 点击寻人按钮，弹出寻人对话框，用户可上传照片、人物信息等，然后点击确定按钮后向后台发布寻人指令，后台返回结果，展示到前端。

应用管理按钮（需要管理员权限）： 点击应用管理按钮，弹出应用部署设置对话框，用户选择应用名称、部署结点等（取决于后端K3S部署yaml文件的需要），点击确定按钮后后台部署应用，前段展示部署是否成。

信息采集：点击信息采集按钮，跳转到grafana页面

#### 原型图以及功能：
原型图链接：https://www.figma.com/file/tewMlPoU9Q7rJaKQsN2RMx/2?node-id=0%3A1

首页：

![](https://pad.degrowth.net/uploads/upload_a985da832893e689c3fbb183a0512a67.png)


目前首页展示的为二楼平面图（后期拓展为学院楼3D模型，点击具体层后跳转到该层平面图），图中显示该楼层房间布局以及房间号，并在图中标识出每个节点的位置（WIFI定位），鼠标指针移动到节点上，展示节点的简略信息，点击节点跳转到节点详细信息页面。
简略信息包括：（1）节点类型，树莓派或OpenWrt （2）节点ip地址
平面图的右侧为登录窗口，页面上方为各个按钮

节点详情：

![](https://pad.degrowth.net/uploads/upload_9e929982ebc8a2db0ef0360fc427b537.png)

![](https://pad.degrowth.net/uploads/upload_758c86320e6e4af65b8fcc0dc6db68e3.png)
节点应包括两种类型（1）树莓派节点，节点详情页面展示节点连接的传感器列表以及应用列表，点击传感器可获得读数，若节点上连接有摄像头，则还需显示摄像头画面。（2）OpenWrt节点，显示OpenWRT节点上获取的网络拓扑图。

目标搜寻界面：

![](https://pad.degrowth.net/uploads/upload_49e2a8ce050a6154ba350c2c2dfc1799.png)


点击按钮进入界面，输入目标信息后（可上传图片），目标信息由人物信息结构化存储调研结果决定，后端调用人脸识别应用进行目标搜索。

应用管理界面：

![](https://pad.degrowth.net/uploads/upload_e8750dc5c021eb2dd6155d371d52e864.png)


点击进入界面，用户选中应用名称和部署的节点，后台根据用户需求生成yaml文件在K3S集群中部署，返回部署结果（成功、失败）

Grafana界面：
![](https://pad.degrowth.net/uploads/upload_e76f798fd63426116bd1b0e892861c91.png)

## ....其他应用</div>
    <div class="ui-toc dropup unselectable hidden-print" style="display:none;">
        <div class="pull-right dropdown">
            <a id="tocLabel" class="ui-toc-label btn btn-default" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false" title="Table of content">
                <i class="fa fa-bars"></i>
            </a>
            <ul id="ui-toc" class="ui-toc-dropdown dropdown-menu" aria-labelledby="tocLabel">
            </ul>
        </div>
    </div>
    <div id="ui-toc-affix" class="ui-affix-toc ui-toc-dropdown unselectable hidden-print" data-spy="affix" style="display:none;"></div>
    
</body>

</html>
<script src="https://pad.degrowth.net/js/mathjax-config-extra.js"></script>

<script src="https://pad.degrowth.net/build/MathJax/MathJax.js" defer></script>
<script src="https://pad.degrowth.net/build/MathJax/config/TeX-AMS-MML_HTMLorMML.js" defer></script>
<script src="https://pad.degrowth.net/build/MathJax/config/Safe.js" defer></script>
<script src="config"></script><script src="build/common.becb2d94d936ce9eb683.js" defer="defer"></script><script src="build/pretty-pack.ae39eb940275f427573c.js" defer="defer"></script>



